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#[derive(Parser, Clone, Derivative)]
43#[derivative(Debug(bound = ""))]
44#[command(version = build_version())]
45pub struct Options {
46 #[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 #[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 #[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 #[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 #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_HEARTBEAT_INTERVAL", default_value = "1s", value_parser = parse_duration)]
84 pub libp2p_heartbeat_interval: Duration,
85
86 #[clap(
88 long,
89 env = "ESPRESSO_SEQUENCER_LIBP2P_HISTORY_GOSSIP",
90 default_value = "3"
91 )]
92 pub libp2p_history_gossip: usize,
93
94 #[clap(
96 long,
97 env = "ESPRESSO_SEQUENCER_LIBP2P_HISTORY_LENGTH",
98 default_value = "5"
99 )]
100 pub libp2p_history_length: usize,
101
102 #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_MESH_N", default_value = "8")]
104 pub libp2p_mesh_n: usize,
105
106 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_MAX_MESSAGES_PER_RPC")]
160 pub libp2p_max_messages_per_rpc: Option<usize>,
161
162 #[clap(
164 long,
165 env = "ESPRESSO_SEQUENCER_LIBP2P_GOSSIP_RETRANSMISSION",
166 default_value = "3"
167 )]
168 pub libp2p_gossip_retransmission: u32,
169
170 #[clap(
172 long,
173 env = "ESPRESSO_SEQUENCER_LIBP2P_FLOOD_PUBLISH",
174 default_value = "true"
175 )]
176 pub libp2p_flood_publish: bool,
177
178 #[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 #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_FANOUT_TTL", default_value = "60s", value_parser = parse_duration)]
184 pub libp2p_fanout_ttl: Duration,
185
186 #[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 #[clap(
192 long,
193 env = "ESPRESSO_SEQUENCER_LIBP2P_GOSSIP_FACTOR",
194 default_value = "0.25"
195 )]
196 pub libp2p_gossip_factor: f64,
197
198 #[clap(
200 long,
201 env = "ESPRESSO_SEQUENCER_LIBP2P_GOSSIP_LAZY",
202 default_value = "6"
203 )]
204 pub libp2p_gossip_lazy: usize,
205
206 #[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 #[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 #[clap(long, env = "ESPRESSO_SEQUENCER_PUBLIC_API_URL")]
225 pub public_api_url: Option<Url>,
226
227 #[clap(
230 long,
231 env = "ESPRESSO_SEQUENCER_LIBP2P_ADVERTISE_ADDRESS",
232 default_value = "localhost:1769"
233 )]
234 pub libp2p_advertise_address: String,
235
236 #[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 #[clap(long, env = "ESPRESSO_SEQUENCER_BUILDER_URLS", value_delimiter = ',')]
250 pub builder_urls: Vec<Url>,
251
252 #[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 #[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 #[clap(raw = true)]
287 modules: Vec<String>,
288
289 #[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 #[clap(flatten)]
302 pub l1_options: L1ClientOptions,
303
304 #[clap(long, env = "ESPRESSO_SEQUENCER_IS_DA", action)]
306 pub is_da: bool,
307
308 #[clap(long, env = "ESPRESSO_SEQUENCER_STATE_PEERS", value_delimiter = ',')]
310 #[derivative(Debug(format_with = "fmt_urls"))]
311 pub state_peers: Vec<Url>,
312
313 #[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 #[clap(flatten)]
325 pub catchup_backoff: BackoffParams,
326
327 #[clap(long, env = "ESPRESSO_SEQUENCER_CATCHUP_BASE_TIMEOUT", default_value = "2s", value_parser = parse_duration)]
331 pub catchup_base_timeout: Duration,
332
333 #[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#[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
400fn 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
415fn 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 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 #[clap(raw = true)]
562 modules: Vec<String>,
563}
564
565impl<Options: ModuleInfo> Module<Options> {
566 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 Http(Module<api::options::Http>),
601 Storage(Module<persistence::fs::Options>),
603 StorageFs(Module<persistence::fs::Options>),
605 StorageSql(Module<persistence::sql::Options>),
607 Query(Module<api::options::Query>),
611 Submit(Module<api::options::Submit>),
615 Status(Module<api::options::Status>),
619 Catchup(Module<api::options::Catchup>),
623 Config(Module<api::options::Config>),
625
626 HotshotEvents(Module<api::options::HotshotEvents>),
630 Explorer(Module<api::options::Explorer>),
634 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}