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#[derive(Parser, Clone, Derivative)]
46#[derivative(Debug(bound = ""))]
47pub struct Options {
48 #[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 #[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 #[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 #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_HEARTBEAT_INTERVAL", default_value = "1s", value_parser = parse_duration)]
78 pub libp2p_heartbeat_interval: Duration,
79
80 #[clap(
82 long,
83 env = "ESPRESSO_SEQUENCER_LIBP2P_HISTORY_GOSSIP",
84 default_value = "3"
85 )]
86 pub libp2p_history_gossip: usize,
87
88 #[clap(
90 long,
91 env = "ESPRESSO_SEQUENCER_LIBP2P_HISTORY_LENGTH",
92 default_value = "5"
93 )]
94 pub libp2p_history_length: usize,
95
96 #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_MESH_N", default_value = "8")]
98 pub libp2p_mesh_n: usize,
99
100 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_MAX_MESSAGES_PER_RPC")]
154 pub libp2p_max_messages_per_rpc: Option<usize>,
155
156 #[clap(
158 long,
159 env = "ESPRESSO_SEQUENCER_LIBP2P_GOSSIP_RETRANSMISSION",
160 default_value = "3"
161 )]
162 pub libp2p_gossip_retransmission: u32,
163
164 #[clap(
166 long,
167 env = "ESPRESSO_SEQUENCER_LIBP2P_FLOOD_PUBLISH",
168 default_value = "true"
169 )]
170 pub libp2p_flood_publish: bool,
171
172 #[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 #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_FANOUT_TTL", default_value = "60s", value_parser = parse_duration)]
178 pub libp2p_fanout_ttl: Duration,
179
180 #[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 #[clap(
186 long,
187 env = "ESPRESSO_SEQUENCER_LIBP2P_GOSSIP_FACTOR",
188 default_value = "0.25"
189 )]
190 pub libp2p_gossip_factor: f64,
191
192 #[clap(
194 long,
195 env = "ESPRESSO_SEQUENCER_LIBP2P_GOSSIP_LAZY",
196 default_value = "6"
197 )]
198 pub libp2p_gossip_lazy: usize,
199
200 #[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 #[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 #[clap(long, env = "ESPRESSO_SEQUENCER_PUBLIC_API_URL")]
219 pub public_api_url: Option<Url>,
220
221 #[clap(
224 long,
225 env = "ESPRESSO_SEQUENCER_LIBP2P_ADVERTISE_ADDRESS",
226 default_value = "localhost:1769"
227 )]
228 pub libp2p_advertise_address: String,
229
230 #[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 #[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 #[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 #[clap(long, name = "KEY_FILE", env = "ESPRESSO_SEQUENCER_KEY_FILE")]
268 pub key_file: Option<PathBuf>,
269
270 #[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 #[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 #[clap(raw = true)]
306 modules: Vec<String>,
307
308 #[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 #[clap(flatten)]
321 pub l1_options: L1ClientOptions,
322
323 #[clap(long, env = "ESPRESSO_SEQUENCER_IS_DA", action)]
325 pub is_da: bool,
326
327 #[clap(long, env = "ESPRESSO_SEQUENCER_STATE_PEERS", value_delimiter = ',')]
329 #[derivative(Debug(format_with = "fmt_urls"))]
330 pub state_peers: Vec<Url>,
331
332 #[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 #[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#[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
420fn get_default_node_type() -> String {
423 format!("espresso-sequencer {}", env!("CARGO_PKG_VERSION"))
424}
425
426fn 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 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 #[clap(raw = true)]
569 modules: Vec<String>,
570}
571
572impl<Options: ModuleInfo> Module<Options> {
573 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 Http(Module<api::options::Http>),
608 Storage(Module<persistence::fs::Options>),
610 StorageFs(Module<persistence::fs::Options>),
612 StorageSql(Module<persistence::sql::Options>),
614 Query(Module<api::options::Query>),
618 Submit(Module<api::options::Submit>),
622 Status(Module<api::options::Status>),
626 Catchup(Module<api::options::Catchup>),
630 Config(Module<api::options::Config>),
632
633 HotshotEvents(Module<api::options::HotshotEvents>),
637 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}