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(long, env = "ESPRESSO_SEQUENCER_BUILDER_URLS", value_delimiter = ',')]
244 pub builder_urls: Vec<Url>,
245
246 #[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 #[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 #[clap(long, name = "KEY_FILE", env = "ESPRESSO_SEQUENCER_KEY_FILE")]
272 pub key_file: Option<PathBuf>,
273
274 #[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 #[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 #[clap(raw = true)]
310 modules: Vec<String>,
311
312 #[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 #[clap(flatten)]
325 pub l1_options: L1ClientOptions,
326
327 #[clap(long, env = "ESPRESSO_SEQUENCER_IS_DA", action)]
329 pub is_da: bool,
330
331 #[clap(long, env = "ESPRESSO_SEQUENCER_STATE_PEERS", value_delimiter = ',')]
333 #[derivative(Debug(format_with = "fmt_urls"))]
334 pub state_peers: Vec<Url>,
335
336 #[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 #[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#[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
439fn get_default_node_type() -> String {
442 format!("espresso-sequencer {}", env!("CARGO_PKG_VERSION"))
443}
444
445fn 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 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 #[clap(raw = true)]
592 modules: Vec<String>,
593}
594
595impl<Options: ModuleInfo> Module<Options> {
596 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 Http(Module<api::options::Http>),
631 Storage(Module<persistence::fs::Options>),
633 StorageFs(Module<persistence::fs::Options>),
635 StorageSql(Module<persistence::sql::Options>),
637 Query(Module<api::options::Query>),
641 Submit(Module<api::options::Submit>),
645 Status(Module<api::options::Status>),
649 Catchup(Module<api::options::Catchup>),
653 Config(Module<api::options::Config>),
655
656 HotshotEvents(Module<api::options::HotshotEvents>),
660 Explorer(Module<api::options::Explorer>),
664 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}