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_telemetry::TelemetryOptions;
16use espresso_types::{BackoffParams, L1ClientOptions, parse_duration};
17use espresso_utils::logging;
18use hotshot_types::addr::NetAddr;
19use libp2p::Multiaddr;
20use light_client::{state::LightClientOptions, storage::LightClientSqliteOptions};
21use serde::Serialize;
22use url::Url;
23
24use crate::{
25 api,
26 genesis::{Genesis, GenesisSource},
27 keyset::KeySetOptions,
28 persistence,
29 proposal_fetcher::ProposalFetcherConfig,
30};
31
32#[derive(Parser, Clone, Derivative)]
52#[derivative(Debug(bound = ""))]
53#[command(version = build_version())]
54pub struct Options {
55 #[clap(
57 short,
58 long,
59 env = "ESPRESSO_NODE_ORCHESTRATOR_URL",
60 default_value = "http://localhost:8080"
61 )]
62 #[derivative(Debug(format_with = "Display::fmt"))]
63 pub orchestrator_url: Url,
64
65 #[clap(
68 short,
69 long,
70 env = "ESPRESSO_NODE_CDN_ENDPOINT",
71 default_value = "127.0.0.1:8081"
72 )]
73 pub cdn_endpoint: String,
74
75 #[clap(
77 long,
78 env = "ESPRESSO_NODE_CLIQUENET_BIND_ADDRESS",
79 default_value = "0.0.0.0:9977",
80 value_parser = parse_bind_addr
81 )]
82 pub cliquenet_bind_address: NetAddr,
83
84 #[clap(
89 long,
90 env = "ESPRESSO_NODE_CLIQUENET_ADVERTISE_ADDRESS",
91 value_parser = parse_advertise_addr
92 )]
93 pub cliquenet_advertise_address: Option<NetAddr>,
94
95 #[clap(
97 long,
98 env = "ESPRESSO_NODE_LIBP2P_BIND_ADDRESS",
99 default_value = "0.0.0.0:1769"
100 )]
101 pub libp2p_bind_address: String,
102
103 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_HEARTBEAT_INTERVAL", default_value = "1s", value_parser = parse_duration)]
105 pub libp2p_heartbeat_interval: Duration,
106
107 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_HISTORY_GOSSIP", default_value = "3")]
109 pub libp2p_history_gossip: usize,
110
111 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_HISTORY_LENGTH", default_value = "5")]
113 pub libp2p_history_length: usize,
114
115 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_MESH_N", default_value = "8")]
117 pub libp2p_mesh_n: usize,
118
119 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_MESH_N_HIGH", default_value = "12")]
121 pub libp2p_mesh_n_high: usize,
122
123 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_MESH_N_LOW", default_value = "6")]
125 pub libp2p_mesh_n_low: usize,
126
127 #[clap(
129 long,
130 env = "ESPRESSO_NODE_LIBP2P_MESH_OUTBOUND_MIN",
131 default_value = "2"
132 )]
133 pub libp2p_mesh_outbound_min: usize,
134
135 #[clap(
137 long,
138 env = "ESPRESSO_NODE_LIBP2P_MAX_IHAVE_LENGTH",
139 default_value = "5000"
140 )]
141 pub libp2p_max_ihave_length: usize,
142
143 #[clap(
145 long,
146 env = "ESPRESSO_NODE_LIBP2P_MAX_IHAVE_MESSAGES",
147 default_value = "10"
148 )]
149 pub libp2p_max_ihave_messages: usize,
150
151 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_PUBLISHED_MESSAGE_IDS_CACHE_TIME", default_value = "10s", value_parser = parse_duration)]
153 pub libp2p_published_message_ids_cache_time: Duration,
154
155 #[clap(
157 long,
158 env = "ESPRESSO_NODE_LIBP2P_MAX_IWANT_FOLLOWUP_TIME",
159 default_value = "3s", value_parser = parse_duration
160 )]
161 pub libp2p_iwant_followup_time: Duration,
162
163 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_MAX_MESSAGES_PER_RPC")]
165 pub libp2p_max_messages_per_rpc: Option<usize>,
166
167 #[clap(
169 long,
170 env = "ESPRESSO_NODE_LIBP2P_GOSSIP_RETRANSMISSION",
171 default_value = "3"
172 )]
173 pub libp2p_gossip_retransmission: u32,
174
175 #[clap(
177 long,
178 env = "ESPRESSO_NODE_LIBP2P_FLOOD_PUBLISH",
179 default_value = "true"
180 )]
181 pub libp2p_flood_publish: bool,
182
183 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_DUPLICATE_CACHE_TIME", default_value = "20m", value_parser = parse_duration)]
185 pub libp2p_duplicate_cache_time: Duration,
186
187 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_FANOUT_TTL", default_value = "60s", value_parser = parse_duration)]
189 pub libp2p_fanout_ttl: Duration,
190
191 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_HEARTBEAT_INITIAL_DELAY", default_value = "5s", value_parser = parse_duration)]
193 pub libp2p_heartbeat_initial_delay: Duration,
194
195 #[clap(
197 long,
198 env = "ESPRESSO_NODE_LIBP2P_GOSSIP_FACTOR",
199 default_value = "0.25"
200 )]
201 pub libp2p_gossip_factor: f64,
202
203 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_GOSSIP_LAZY", default_value = "6")]
205 pub libp2p_gossip_lazy: usize,
206
207 #[clap(
209 long,
210 env = "ESPRESSO_NODE_LIBP2P_MAX_GOSSIP_TRANSMIT_SIZE",
211 default_value = "2000000"
212 )]
213 pub libp2p_max_gossip_transmit_size: usize,
214
215 #[clap(
217 long,
218 env = "ESPRESSO_NODE_LIBP2P_MAX_DIRECT_TRANSMIT_SIZE",
219 default_value = "20000000"
220 )]
221 pub libp2p_max_direct_transmit_size: u64,
222
223 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_DHT_PUT_QUORUM")]
225 pub libp2p_dht_put_quorum: Option<std::num::NonZeroUsize>,
226
227 #[clap(long, env = "ESPRESSO_NODE_PUBLIC_API_URL")]
230 pub public_api_url: Option<Url>,
231
232 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_ADVERTISE_ADDRESS")]
245 pub libp2p_advertise_address: Option<String>,
246
247 #[clap(
252 long,
253 env = "ESPRESSO_NODE_LIBP2P_BOOTSTRAP_NODES",
254 value_delimiter = ',',
255 num_args = 1..
256 )]
257 pub libp2p_bootstrap_nodes: Option<Vec<Multiaddr>>,
258
259 #[clap(long, env = "ESPRESSO_BUILDER_URLS", value_delimiter = ',')]
261 pub builder_urls: Vec<Url>,
262
263 #[clap(
265 long,
266 env = "ESPRESSO_STATE_RELAY_SERVER_URL",
267 default_value = "http://localhost:8083"
268 )]
269 #[derivative(Debug(format_with = "Display::fmt"))]
270 pub state_relay_server_url: Url,
271
272 #[clap(
276 long,
277 name = "GENESIS_FILE",
278 env = "ESPRESSO_NODE_GENESIS_FILE",
279 default_value = "/genesis/demo.toml"
280 )]
281 pub genesis_file: GenesisSource,
282
283 #[clap(flatten)]
284 pub key_set: KeySetOptions,
285
286 #[clap(raw = true)]
300 modules: Vec<String>,
301
302 #[clap(
304 long,
305 env = "ESPRESSO_L1_PROVIDER",
306 default_value = "http://localhost:8545",
307 value_delimiter = ',',
308 num_args = 1..,
309 )]
310 #[derivative(Debug = "ignore")]
311 pub l1_provider_url: Vec<Url>,
312
313 #[clap(flatten)]
315 pub l1_options: L1ClientOptions,
316
317 #[clap(long, env = "ESPRESSO_NODE_IS_DA", action)]
319 pub is_da: bool,
320
321 #[clap(long, env = "ESPRESSO_NODE_STATE_PEERS", value_delimiter = ',')]
323 #[derivative(Debug(format_with = "fmt_urls"))]
324 pub state_peers: Vec<Url>,
325
326 #[clap(long, env = "ESPRESSO_NODE_CONFIG_PEERS", value_delimiter = ',')]
333 #[derivative(Debug(format_with = "fmt_opt_urls"))]
334 pub config_peers: Option<Vec<Url>>,
335
336 #[clap(flatten)]
338 pub catchup_backoff: BackoffParams,
339
340 #[clap(long, env = "ESPRESSO_NODE_CATCHUP_BASE_TIMEOUT", default_value = "2s", value_parser = parse_duration)]
344 pub catchup_base_timeout: Duration,
345
346 #[clap(long, env = "ESPRESSO_NODE_LOCAL_CATCHUP_TIMEOUT", default_value = "5s", value_parser = parse_duration)]
351 pub local_catchup_timeout: Duration,
352
353 #[clap(long, env = "ESPRESSO_NODE_BOOTSTRAP_EPOCH_CATCHUP_TIMEOUT", default_value = "30s", value_parser = parse_duration)]
359 pub bootstrap_epoch_catchup_timeout: Duration,
360
361 #[clap(flatten)]
362 pub logging: logging::Config,
363
364 #[clap(flatten)]
365 pub identity: Identity,
366
367 #[clap(flatten)]
368 pub telemetry: TelemetryOptions,
369
370 #[clap(flatten)]
371 pub proposal_fetcher_config: ProposalFetcherConfig,
372}
373
374impl Options {
375 pub fn modules(&self) -> Modules {
376 ModuleArgs(self.modules.clone()).parse()
377 }
378}
379
380fn parse_bind_addr(s: &str) -> Result<NetAddr, String> {
383 let addr = s.parse::<NetAddr>().map_err(|e| e.to_string())?;
384 addr.validate().map_err(|e| e.to_string())?;
385 Ok(addr)
386}
387
388fn parse_advertise_addr(s: &str) -> Result<NetAddr, String> {
392 let addr = parse_bind_addr(s)?;
393 if addr.port() == 0 {
394 return Err("port 0 is not a port a peer can connect to".to_string());
395 }
396 Ok(addr)
397}
398
399#[derive(Parser, Clone, Derivative, Serialize)]
404#[derivative(Debug(bound = ""))]
405pub struct Identity {
406 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_COUNTRY_CODE")]
407 pub country_code: Option<String>,
408 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_LATITUDE")]
409 pub latitude: Option<f64>,
410 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_LONGITUDE")]
411 pub longitude: Option<f64>,
412
413 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_NODE_NAME")]
414 pub node_name: Option<String>,
415 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_NODE_DESCRIPTION")]
416 pub node_description: Option<String>,
417
418 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_COMPANY_NAME")]
419 pub company_name: Option<String>,
420 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_COMPANY_WEBSITE")]
421 pub company_website: Option<Url>,
422 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_OPERATING_SYSTEM", default_value = std::env::consts::OS)]
423 pub operating_system: Option<String>,
424 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_NODE_TYPE", default_value = get_default_node_type())]
425 pub node_type: Option<String>,
426 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_NETWORK_TYPE")]
427 pub network_type: Option<String>,
428
429 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_14x14_1x")]
430 pub icon_14x14_1x: Option<Url>,
431 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_14x14_2x")]
432 pub icon_14x14_2x: Option<Url>,
433 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_14x14_3x")]
434 pub icon_14x14_3x: Option<Url>,
435 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_24x24_1x")]
436 pub icon_24x24_1x: Option<Url>,
437 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_24x24_2x")]
438 pub icon_24x24_2x: Option<Url>,
439 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_24x24_3x")]
440 pub icon_24x24_3x: Option<Url>,
441}
442
443fn get_default_node_type() -> String {
446 format!("espresso-sequencer {}", env!("CARGO_PKG_VERSION"))
447}
448
449fn build_version() -> String {
450 let info = espresso_utils::build_info!();
451 format!(
452 "{}\nfeatures: {}",
453 info.clap_version(),
454 env!("VERGEN_CARGO_FEATURES"),
455 )
456}
457
458fn fmt_urls(v: &[Url], fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
460 write!(
461 fmt,
462 "{:?}",
463 v.iter().map(|i| i.to_string()).collect::<Vec<_>>()
464 )
465}
466
467fn fmt_opt_urls(
468 v: &Option<Vec<Url>>,
469 fmt: &mut std::fmt::Formatter,
470) -> Result<(), std::fmt::Error> {
471 match v {
472 Some(urls) => {
473 write!(fmt, "Some(")?;
474 fmt_urls(urls, fmt)?;
475 write!(fmt, ")")?;
476 },
477 None => {
478 write!(fmt, "None")?;
479 },
480 }
481 Ok(())
482}
483
484#[derive(Clone, Copy, Debug, PartialEq, Eq)]
485pub struct Ratio {
486 pub numerator: u64,
487 pub denominator: u64,
488}
489
490impl From<Ratio> for (u64, u64) {
491 fn from(r: Ratio) -> Self {
492 (r.numerator, r.denominator)
493 }
494}
495
496impl Display for Ratio {
497 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
498 write!(f, "{}:{}", self.numerator, self.denominator)
499 }
500}
501
502impl PartialOrd for Ratio {
503 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
504 Some(self.cmp(other))
505 }
506}
507
508impl Ord for Ratio {
509 fn cmp(&self, other: &Self) -> Ordering {
510 (self.numerator * other.denominator).cmp(&(other.numerator * self.denominator))
511 }
512}
513
514#[derive(Clone, Debug)]
515struct ModuleArgs(Vec<String>);
516
517impl ModuleArgs {
518 fn parse(&self) -> Modules {
519 match self.try_parse() {
520 Ok(modules) => modules,
521 Err(err) => err.exit(),
522 }
523 }
524
525 fn try_parse(&self) -> Result<Modules, clap::Error> {
526 let mut modules = Modules::default();
527 let mut curr = self.0.clone();
528 let mut provided = Default::default();
529
530 while !curr.is_empty() {
531 let module = SequencerModule::try_parse_from(
535 once("sequencer --").chain(curr.iter().map(|s| s.as_str())),
536 )?;
537 match module {
538 SequencerModule::Storage(m) => {
539 curr = m.add(&mut modules.storage_fs, &mut provided)?
540 },
541 SequencerModule::StorageFs(m) => {
542 curr = m.add(&mut modules.storage_fs, &mut provided)?
543 },
544 SequencerModule::StorageSql(m) => {
545 curr = m.add(&mut modules.storage_sql, &mut provided)?
546 },
547 SequencerModule::Http(m) => curr = m.add(&mut modules.http, &mut provided)?,
548 SequencerModule::Query(m) => curr = m.add(&mut modules.query, &mut provided)?,
549 SequencerModule::Submit(m) => curr = m.add(&mut modules.submit, &mut provided)?,
550 SequencerModule::Status(m) => curr = m.add(&mut modules.status, &mut provided)?,
551 SequencerModule::Catchup(m) => curr = m.add(&mut modules.catchup, &mut provided)?,
552 SequencerModule::Config(m) => curr = m.add(&mut modules.config, &mut provided)?,
553 SequencerModule::HotshotEvents(m) => {
554 curr = m.add(&mut modules.hotshot_events, &mut provided)?
555 },
556 SequencerModule::Explorer(m) => {
557 curr = m.add(&mut modules.explorer, &mut provided)?
558 },
559 SequencerModule::LightClient(m) => {
560 curr = m.add(&mut modules.light_client, &mut provided)?
561 },
562 }
563 }
564
565 Ok(modules)
566 }
567}
568
569trait ModuleInfo: Args + FromArgMatches {
570 const NAME: &'static str;
571 fn requires() -> Vec<&'static str>;
572}
573
574macro_rules! module {
575 ($name:expr, $opt:ty $(,requires: $($req:expr),*)?) => {
576 impl ModuleInfo for $opt {
577 const NAME: &'static str = $name;
578
579 fn requires() -> Vec<&'static str> {
580 vec![$($($req),*)?]
581 }
582 }
583 };
584}
585
586module!("storage-fs", persistence::fs::Options);
587module!("storage-sql", persistence::sql::Options);
588module!("http", api::options::Http);
589module!("query", api::options::Query, requires: "http");
590module!("submit", api::options::Submit, requires: "http");
591module!("status", api::options::Status, requires: "http");
592module!("catchup", api::options::Catchup, requires: "http");
593module!("config", api::options::Config, requires: "http");
594module!("hotshot-events", api::options::HotshotEvents, requires: "http");
595module!("explorer", api::options::Explorer, requires: "http", "storage-sql");
596module!("light-client", api::options::LightClient, requires: "http", "storage-sql");
597
598#[derive(Clone, Debug, Args)]
599struct Module<Options: ModuleInfo> {
600 #[clap(flatten)]
601 options: Box<Options>,
602
603 #[clap(raw = true)]
605 modules: Vec<String>,
606}
607
608impl<Options: ModuleInfo> Module<Options> {
609 fn add(
611 self,
612 options: &mut Option<Options>,
613 provided: &mut HashSet<&'static str>,
614 ) -> Result<Vec<String>, clap::Error> {
615 if options.is_some() {
616 return Err(clap::Error::raw(
617 ErrorKind::TooManyValues,
618 format!("optional module {} can only be started once", Options::NAME),
619 ));
620 }
621 for req in Options::requires() {
622 if !provided.contains(&req) {
623 return Err(clap::Error::raw(
624 ErrorKind::MissingRequiredArgument,
625 format!("module {} is missing required module {req}", Options::NAME),
626 ));
627 }
628 }
629 *options = Some(*self.options);
630 provided.insert(Options::NAME);
631 Ok(self.modules)
632 }
633}
634
635#[derive(Clone, Debug, Parser)]
636enum SequencerModule {
637 Http(Module<api::options::Http>),
644 Storage(Module<persistence::fs::Options>),
646 StorageFs(Module<persistence::fs::Options>),
648 StorageSql(Module<persistence::sql::Options>),
650 Query(Module<api::options::Query>),
654 Submit(Module<api::options::Submit>),
658 Status(Module<api::options::Status>),
662 Catchup(Module<api::options::Catchup>),
666 Config(Module<api::options::Config>),
668
669 HotshotEvents(Module<api::options::HotshotEvents>),
673 Explorer(Module<api::options::Explorer>),
677 LightClient(Module<api::options::LightClient>),
684}
685
686#[derive(Clone, Debug, Default)]
687pub struct Modules {
688 pub storage_fs: Option<persistence::fs::Options>,
689 pub storage_sql: Option<persistence::sql::Options>,
690 pub http: Option<api::options::Http>,
691 pub query: Option<api::options::Query>,
692 pub submit: Option<api::options::Submit>,
693 pub status: Option<api::options::Status>,
694 pub catchup: Option<api::options::Catchup>,
695 pub config: Option<api::options::Config>,
696 pub hotshot_events: Option<api::options::HotshotEvents>,
697 pub explorer: Option<api::options::Explorer>,
698 pub light_client: Option<api::options::LightClient>,
699}
700
701#[derive(Clone, Debug, Serialize)]
702pub struct PublicNodeConfig {
703 pub orchestrator_url: Url,
704 pub cdn_endpoint: String,
705 pub cliquenet_bind_address: NetAddr,
706 pub cliquenet_advertise_address: Option<NetAddr>,
707 pub libp2p_bind_address: String,
708 pub libp2p_advertise_address: Option<String>,
709 pub libp2p_bootstrap_nodes: Option<Vec<Multiaddr>>,
710 pub public_api_url: Option<Url>,
711 pub builder_urls: Vec<Url>,
712 pub state_relay_server_url: Url,
713 pub state_peers: Vec<Url>,
714 pub config_peers: Option<Vec<Url>>,
715 pub is_da: bool,
716 pub genesis_file: GenesisSource,
717 pub genesis: Genesis,
718 pub identity: Identity,
719 pub catchup_base_timeout: Duration,
720 pub local_catchup_timeout: Duration,
721 pub bootstrap_epoch_catchup_timeout: Duration,
722 pub catchup_backoff: BackoffParams,
723 pub proposal_fetcher: ProposalFetcherConfig,
724 pub libp2p: Libp2pTuning,
725 pub l1: L1Tuning,
726 pub l1_provider_count: usize,
727 pub l1_ws_provider_count: usize,
728 pub storage: StorageConfig,
729 pub modules: ApiModulesConfig,
730}
731
732#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
733#[serde(rename_all = "kebab-case")]
734pub enum StorageBackend {
735 Sql,
736 Fs,
737 FsDefault,
738}
739
740#[derive(Clone, Debug, Serialize)]
741pub struct StorageConfig {
742 pub backend: StorageBackend,
744 pub fs: Option<FsStorageConfig>,
745 pub sql: Option<SqlStorageConfig>,
746}
747
748#[derive(Clone, Debug, Serialize)]
749pub struct FsStorageConfig {
750 pub path: PathBuf,
751 pub consensus_view_retention: u64,
752}
753
754#[derive(Clone, Debug, Serialize)]
755pub struct SqlStorageConfig {
756 pub prune: bool,
757 pub archive: bool,
758 pub lightweight: bool,
759 pub disable_proactive_fetching: bool,
760 pub fetch_rate_limit: Option<usize>,
761 pub active_fetch_delay: Option<Duration>,
762 pub chunk_fetch_delay: Option<Duration>,
763 pub sync_status_chunk_size: Option<usize>,
764 pub sync_status_ttl: Option<Duration>,
765 pub proactive_scan_chunk_size: Option<usize>,
766 pub proactive_scan_interval: Option<Duration>,
767 pub idle_connection_timeout: Duration,
768 pub connection_timeout: Duration,
769 pub slow_statement_threshold: Duration,
770 pub statement_timeout: Duration,
771 pub min_connections: u32,
772 pub max_connections: u32,
773 pub query_min_connections: Option<u32>,
774 pub query_max_connections: Option<u32>,
775 pub pruning: PruningView,
776 pub consensus_pruning: ConsensusPruningView,
777}
778
779#[derive(Clone, Debug, Serialize)]
780pub struct PruningView {
781 pub pruning_threshold: Option<u64>,
782 pub minimum_retention: Option<Duration>,
783 pub target_retention: Option<Duration>,
784 pub batch_size: Option<u64>,
785 pub max_usage: Option<u16>,
786 pub interval: Option<Duration>,
787 pub pages: Option<u64>,
788}
789
790#[derive(Clone, Debug, Serialize)]
791pub struct ConsensusPruningView {
792 pub target_retention: u64,
793 pub minimum_retention: u64,
794 pub target_usage: u64,
795}
796
797#[derive(Clone, Debug, Serialize)]
798pub struct ApiModulesConfig {
799 pub http: Option<HttpConfig>,
800 pub query: Option<QueryConfig>,
801 pub submit: bool,
802 pub status: bool,
803 pub catchup: bool,
804 pub config: bool,
805 pub hotshot_events: bool,
806 pub explorer: bool,
807 pub light_client: bool,
808}
809
810#[derive(Clone, Debug, Serialize)]
811pub struct HttpConfig {
812 pub port: u16,
813 pub max_connections: Option<usize>,
814 pub tonic_port: Option<u16>,
815}
816
817#[derive(Clone, Debug, Serialize)]
818pub struct QueryConfig {
819 pub peers: Vec<Url>,
820 pub light_client: LightClientOptions,
821 pub light_client_db: LightClientSqliteOptions,
822}
823
824impl From<&persistence::sql::PruningOptions> for PruningView {
825 fn from(o: &persistence::sql::PruningOptions) -> Self {
826 Self {
827 pruning_threshold: o.pruning_threshold,
828 minimum_retention: o.minimum_retention,
829 target_retention: o.target_retention,
830 batch_size: o.batch_size,
831 max_usage: o.max_usage,
832 interval: o.interval,
833 pages: o.pages,
834 }
835 }
836}
837
838impl From<&persistence::sql::ConsensusPruningOptions> for ConsensusPruningView {
839 fn from(o: &persistence::sql::ConsensusPruningOptions) -> Self {
840 Self {
841 target_retention: o.target_retention,
842 minimum_retention: o.minimum_retention,
843 target_usage: o.target_usage,
844 }
845 }
846}
847
848impl From<&persistence::sql::Options> for SqlStorageConfig {
849 fn from(o: &persistence::sql::Options) -> Self {
850 Self {
851 prune: o.prune,
852 archive: o.archive,
853 lightweight: o.lightweight,
854 disable_proactive_fetching: o.disable_proactive_fetching,
855 fetch_rate_limit: o.fetch_rate_limit,
856 active_fetch_delay: o.active_fetch_delay,
857 chunk_fetch_delay: o.chunk_fetch_delay,
858 sync_status_chunk_size: o.sync_status_chunk_size,
859 sync_status_ttl: o.sync_status_ttl,
860 proactive_scan_chunk_size: o.proactive_scan_chunk_size,
861 proactive_scan_interval: o.proactive_scan_interval,
862 idle_connection_timeout: o.idle_connection_timeout,
863 connection_timeout: o.connection_timeout,
864 slow_statement_threshold: o.slow_statement_threshold,
865 statement_timeout: o.statement_timeout,
866 min_connections: o.min_connections,
867 max_connections: o.max_connections,
868 #[cfg(not(feature = "embedded-db"))]
869 query_min_connections: o.query_min_connections,
870 #[cfg(feature = "embedded-db")]
871 query_min_connections: None,
872 #[cfg(not(feature = "embedded-db"))]
873 query_max_connections: o.query_max_connections,
874 #[cfg(feature = "embedded-db")]
875 query_max_connections: None,
876 pruning: PruningView::from(&o.pruning),
877 consensus_pruning: ConsensusPruningView::from(&o.consensus_pruning),
878 }
879 }
880}
881
882impl From<&persistence::fs::Options> for FsStorageConfig {
883 fn from(o: &persistence::fs::Options) -> Self {
884 Self {
885 path: o.path.clone(),
886 consensus_view_retention: o.consensus_view_retention,
887 }
888 }
889}
890
891impl From<&api::options::Http> for HttpConfig {
892 fn from(o: &api::options::Http) -> Self {
893 Self {
894 port: o.port,
895 max_connections: o.max_connections,
896 tonic_port: o.tonic_port,
897 }
898 }
899}
900
901impl From<&api::options::Query> for QueryConfig {
902 fn from(o: &api::options::Query) -> Self {
903 Self {
904 peers: o.peers.clone(),
905 light_client: o.light_client.clone(),
906 light_client_db: o.light_client_db.clone(),
907 }
908 }
909}
910
911impl From<&Modules> for ApiModulesConfig {
912 fn from(m: &Modules) -> Self {
913 Self {
914 http: m.http.as_ref().map(HttpConfig::from),
915 query: m.query.as_ref().map(QueryConfig::from),
916 submit: m.submit.is_some(),
917 status: m.status.is_some(),
918 catchup: m.catchup.is_some(),
919 config: m.config.is_some(),
920 hotshot_events: m.hotshot_events.is_some(),
921 explorer: m.explorer.is_some(),
922 light_client: m.light_client.is_some(),
923 }
924 }
925}
926
927#[derive(Clone, Debug, Serialize)]
928pub struct Libp2pTuning {
929 pub heartbeat_interval: Duration,
930 pub heartbeat_initial_delay: Duration,
931 pub history_gossip: usize,
932 pub history_length: usize,
933 pub mesh_n: usize,
934 pub mesh_n_high: usize,
935 pub mesh_n_low: usize,
936 pub mesh_outbound_min: usize,
937 pub max_ihave_length: usize,
938 pub max_ihave_messages: usize,
939 pub published_message_ids_cache_time: Duration,
940 pub iwant_followup_time: Duration,
941 pub max_messages_per_rpc: Option<usize>,
942 pub gossip_retransmission: u32,
943 pub gossip_factor: f64,
944 pub gossip_lazy: usize,
945 pub max_gossip_transmit_size: usize,
946 pub max_direct_transmit_size: u64,
947 pub fanout_ttl: Duration,
948 pub duplicate_cache_time: Duration,
949 pub flood_publish: bool,
950}
951
952#[derive(Clone, Debug, Serialize)]
953pub struct L1Tuning {
954 pub retry_delay: Duration,
955 pub polling_interval: Duration,
956 pub blocks_cache_size: usize,
957 pub events_channel_capacity: usize,
958 pub events_max_block_range: u64,
959 pub subscription_timeout: Duration,
960 pub frequent_failure_tolerance: Duration,
961 pub consecutive_failure_tolerance: usize,
962 pub failover_revert: Duration,
963 pub rate_limit_delay: Option<Duration>,
964 pub stake_table_update_interval: Duration,
965 pub events_max_retry_duration: Duration,
966 pub finalized_safety_margin: Option<u64>,
967}
968
969impl From<&Options> for Libp2pTuning {
970 fn from(o: &Options) -> Self {
971 Self {
972 heartbeat_interval: o.libp2p_heartbeat_interval,
973 heartbeat_initial_delay: o.libp2p_heartbeat_initial_delay,
974 history_gossip: o.libp2p_history_gossip,
975 history_length: o.libp2p_history_length,
976 mesh_n: o.libp2p_mesh_n,
977 mesh_n_high: o.libp2p_mesh_n_high,
978 mesh_n_low: o.libp2p_mesh_n_low,
979 mesh_outbound_min: o.libp2p_mesh_outbound_min,
980 max_ihave_length: o.libp2p_max_ihave_length,
981 max_ihave_messages: o.libp2p_max_ihave_messages,
982 published_message_ids_cache_time: o.libp2p_published_message_ids_cache_time,
983 iwant_followup_time: o.libp2p_iwant_followup_time,
984 max_messages_per_rpc: o.libp2p_max_messages_per_rpc,
985 gossip_retransmission: o.libp2p_gossip_retransmission,
986 gossip_factor: o.libp2p_gossip_factor,
987 gossip_lazy: o.libp2p_gossip_lazy,
988 max_gossip_transmit_size: o.libp2p_max_gossip_transmit_size,
989 max_direct_transmit_size: o.libp2p_max_direct_transmit_size,
990 fanout_ttl: o.libp2p_fanout_ttl,
991 duplicate_cache_time: o.libp2p_duplicate_cache_time,
992 flood_publish: o.libp2p_flood_publish,
993 }
994 }
995}
996
997impl From<&L1ClientOptions> for L1Tuning {
998 fn from(o: &L1ClientOptions) -> Self {
999 Self {
1000 retry_delay: o.l1_retry_delay,
1001 polling_interval: o.l1_polling_interval,
1002 blocks_cache_size: o.l1_blocks_cache_size.get(),
1003 events_channel_capacity: o.l1_events_channel_capacity,
1004 events_max_block_range: o.l1_events_max_block_range,
1005 subscription_timeout: o.subscription_timeout,
1006 frequent_failure_tolerance: o.l1_frequent_failure_tolerance,
1007 consecutive_failure_tolerance: o.l1_consecutive_failure_tolerance,
1008 failover_revert: o.l1_failover_revert,
1009 rate_limit_delay: o.l1_rate_limit_delay,
1010 stake_table_update_interval: o.stake_table_update_interval,
1011 events_max_retry_duration: o.l1_events_max_retry_duration,
1012 finalized_safety_margin: o.l1_finalized_safety_margin,
1013 }
1014 }
1015}
1016
1017impl PublicNodeConfig {
1018 pub fn new(opt: &Options, modules: &Modules, genesis: &Genesis) -> Self {
1019 let storage = if let Some(sql) = modules.storage_sql.as_ref() {
1020 StorageConfig {
1021 backend: StorageBackend::Sql,
1022 fs: None,
1023 sql: Some(SqlStorageConfig::from(sql)),
1024 }
1025 } else if let Some(fs) = modules.storage_fs.as_ref() {
1026 StorageConfig {
1027 backend: StorageBackend::Fs,
1028 fs: Some(FsStorageConfig::from(fs)),
1029 sql: None,
1030 }
1031 } else {
1032 let fs = persistence::fs::Options::try_parse_from(std::iter::empty::<String>()).ok();
1033 StorageConfig {
1034 backend: StorageBackend::FsDefault,
1035 fs: fs.as_ref().map(FsStorageConfig::from),
1036 sql: None,
1037 }
1038 };
1039
1040 Self {
1041 orchestrator_url: opt.orchestrator_url.clone(),
1042 cdn_endpoint: opt.cdn_endpoint.clone(),
1043 cliquenet_bind_address: opt.cliquenet_bind_address.clone(),
1044 cliquenet_advertise_address: opt.cliquenet_advertise_address.clone(),
1045 libp2p_bind_address: opt.libp2p_bind_address.clone(),
1046 libp2p_advertise_address: opt.libp2p_advertise_address.clone(),
1047 libp2p_bootstrap_nodes: opt.libp2p_bootstrap_nodes.clone(),
1048 public_api_url: opt.public_api_url.clone(),
1049 builder_urls: opt.builder_urls.clone(),
1050 state_relay_server_url: opt.state_relay_server_url.clone(),
1051 state_peers: opt.state_peers.clone(),
1052 config_peers: opt.config_peers.clone(),
1053 is_da: opt.is_da,
1054 genesis_file: opt.genesis_file.clone(),
1055 genesis: genesis.clone(),
1056 identity: opt.identity.clone(),
1057 catchup_base_timeout: opt.catchup_base_timeout,
1058 local_catchup_timeout: opt.local_catchup_timeout,
1059 bootstrap_epoch_catchup_timeout: opt.bootstrap_epoch_catchup_timeout,
1060 catchup_backoff: opt.catchup_backoff,
1061 proposal_fetcher: opt.proposal_fetcher_config,
1062 libp2p: Libp2pTuning::from(opt),
1063 l1: L1Tuning::from(&opt.l1_options),
1064 l1_provider_count: opt.l1_provider_url.len(),
1065 l1_ws_provider_count: opt
1066 .l1_options
1067 .l1_ws_provider
1068 .as_ref()
1069 .map(|v| v.len())
1070 .unwrap_or(0),
1071 storage,
1072 modules: ApiModulesConfig::from(modules),
1073 }
1074 }
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use alloy::primitives::{Address, B256, U256};
1080 use espresso_types::{
1081 FeeAccount, GenesisHeader, L1BlockInfo, PubKey, SeqTypes, Timestamp, Upgrade, UpgradeMode,
1082 UpgradeType, ViewBasedUpgrade, v0_3::ChainConfig,
1083 };
1084 use espresso_utils::ser::FromStringOrInteger;
1085 use hotshot_types::{
1086 PeerConfig, VersionedDaCommittee, light_client::StateKeyPair, stake_table::StakeTableEntry,
1087 traits::signature_key::SignatureKey, x25519,
1088 };
1089 use tagged_base64::TaggedBase64;
1090 use vbs::version::Version;
1091
1092 use super::*;
1093 use crate::genesis::{L1Finalized, StakeTableConfig};
1094
1095 #[test]
1096 fn test_build_version() {
1097 let version = build_version();
1098 for field in [
1099 "describe:",
1100 "rev:",
1101 "modified:",
1102 "branch:",
1103 "commit-timestamp:",
1104 "debug:",
1105 "os:",
1106 "arch:",
1107 "features:",
1108 ] {
1109 assert!(version.contains(field), "missing {field}: {version}");
1110 }
1111 assert!(
1112 version.contains("debug: true"),
1113 "expected debug build in test: {version}"
1114 );
1115 assert!(
1116 version.contains("testing"),
1117 "expected testing in features: {version}"
1118 );
1119 }
1120
1121 pub(super) fn parse_options_with(extra: &[&str]) -> Options {
1123 let (_, priv_key) = PubKey::generated_from_seed_indexed([0; 32], 0);
1124 let state_key = StateKeyPair::generate_from_seed_indexed([0; 32], 0);
1125 let x25519_kp = x25519::Keypair::generate().unwrap();
1126
1127 let priv_staking = priv_key.to_tagged_base64().expect("valid key").to_string();
1128 let priv_state = state_key
1129 .sign_key_ref()
1130 .to_tagged_base64()
1131 .expect("valid key")
1132 .to_string();
1133 let priv_x25519 = TaggedBase64::try_from(x25519_kp.secret_key())
1134 .expect("valid key")
1135 .to_string();
1136
1137 let mut args: Vec<String> = vec![
1138 "sequencer".into(),
1139 "--private-staking-key".into(),
1140 priv_staking,
1141 "--private-state-key".into(),
1142 priv_state,
1143 "--private-x25519-key".into(),
1144 priv_x25519,
1145 ];
1146 args.extend(extra.iter().map(|s| s.to_string()));
1147
1148 Options::parse_from(args)
1149 }
1150
1151 fn test_genesis() -> Genesis {
1154 let chain_config = ChainConfig {
1155 chain_id: 999999999.into(),
1156 max_block_size: 3000.into(),
1157 base_fee: 1.into(),
1158 fee_recipient: FeeAccount::default(),
1159 fee_contract: Some(Address::from([0x11; 20])),
1160 stake_table_contract: Some(Address::from([0x22; 20])),
1161 };
1162 let peer = PeerConfig::<SeqTypes> {
1163 stake_table_entry: StakeTableEntry {
1164 stake_key: PubKey::generated_from_seed_indexed([0; 32], 0).0,
1165 stake_amount: U256::from(1),
1166 },
1167 state_ver_key: StateKeyPair::generate_from_seed_indexed([0; 32], 0).ver_key(),
1168 connect_info: None,
1169 };
1170 Genesis {
1171 base_version: Version { major: 0, minor: 4 },
1172 upgrade_version: Version { major: 0, minor: 5 },
1173 genesis_version: Version { major: 0, minor: 2 },
1174 epoch_height: Some(40000),
1175 drb_difficulty: Some(25000000000),
1176 drb_upgrade_difficulty: Some(25000000000),
1177 epoch_start_block: Some(10960201),
1178 stake_table_capacity: Some(200),
1179 chain_config,
1180 stake_table: StakeTableConfig { capacity: 200 },
1181 accounts: Default::default(),
1182 l1_finalized: L1Finalized::Block(L1BlockInfo {
1183 number: 21116303,
1184 timestamp: U256::from(1700000000),
1185 hash: B256::from([0x44; 32]),
1186 }),
1187 header: GenesisHeader {
1188 timestamp: Timestamp::from_integer(123456).unwrap(),
1189 chain_config,
1190 },
1191 upgrades: [(
1192 Version { major: 0, minor: 5 },
1193 Upgrade {
1194 mode: UpgradeMode::View(ViewBasedUpgrade {
1195 start_proposing_view: 100,
1196 stop_proposing_view: 200,
1197 start_voting_view: Some(1),
1198 stop_voting_view: Some(300),
1199 }),
1200 upgrade_type: UpgradeType::EpochReward { chain_config },
1201 },
1202 )]
1203 .into_iter()
1204 .collect(),
1205 da_committees: Some(vec![VersionedDaCommittee {
1206 start_version: Version { major: 0, minor: 6 },
1207 start_epoch: 10,
1208 committee: vec![peer],
1209 }]),
1210 }
1211 }
1212
1213 #[test]
1214 fn public_node_config_no_secrets() {
1215 let opt = parse_options_with(&[
1216 "--l1-provider-url",
1217 "https://user:pass@example.invalid/v2/SECRET_API_KEY,https://example2.invalid/key2",
1218 "--cliquenet-bind-address",
1219 "127.0.0.1:9999",
1220 "--state-peers",
1221 "https://peer1.test,https://peer2.test",
1222 ]);
1223 let modules = opt.modules();
1224
1225 let cfg = PublicNodeConfig::new(&opt, &modules, &test_genesis());
1226 let json = serde_json::to_string(&cfg).unwrap();
1227 let json_lc = json.to_lowercase();
1228
1229 assert!(
1230 json.contains("127.0.0.1:9999"),
1231 "CLI override missing from JSON: {json}"
1232 );
1233 assert!(
1234 !json.contains("SECRET_API_KEY"),
1235 "L1 API key leaked into JSON: {json}"
1236 );
1237 assert!(
1238 !json.contains("user:pass"),
1239 "L1 URL userinfo leaked into JSON: {json}"
1240 );
1241 assert!(
1242 !json.contains("example.invalid"),
1243 "L1 host leaked into JSON: {json}"
1244 );
1245 assert!(
1246 !json.contains("example2.invalid"),
1247 "second L1 host leaked into JSON: {json}"
1248 );
1249 assert!(
1250 json.contains("\"l1_provider_count\":2"),
1251 "missing l1_provider_count: {json}"
1252 );
1253 assert!(
1254 !json.contains("\"uri\""),
1255 "DB URI key leaked into JSON: {json}"
1256 );
1257 assert!(
1258 !json.contains("postgres://"),
1259 "DB connection string leaked into JSON: {json}"
1260 );
1261
1262 const FORBIDDEN: &[&str] = &[
1263 "private", "mnemonic", "secret", "x25519", "key_file", "seed", "password",
1264 ];
1265 for token in FORBIDDEN {
1266 assert!(
1267 !json_lc.contains(token),
1268 "forbidden token '{token}' leaked into JSON: {json}"
1269 );
1270 }
1271
1272 assert!(
1273 json.contains("peer1.test") && json.contains("peer2.test"),
1274 "state_peers missing from JSON: {json}"
1275 );
1276 }
1277
1278 #[test]
1279 fn public_node_config_optionals() {
1280 let opt = parse_options_with(&[]);
1281 let modules = opt.modules();
1282
1283 let cfg = PublicNodeConfig::new(&opt, &modules, &test_genesis());
1284
1285 assert!(
1286 cfg.cliquenet_advertise_address.is_none(),
1287 "expected no advertise address: {:?}",
1288 cfg.cliquenet_advertise_address
1289 );
1290 assert!(
1291 cfg.libp2p_bootstrap_nodes.is_none(),
1292 "expected no bootstrap nodes: {:?}",
1293 cfg.libp2p_bootstrap_nodes
1294 );
1295 assert!(
1296 cfg.config_peers.is_none(),
1297 "expected no config peers: {:?}",
1298 cfg.config_peers
1299 );
1300 assert_eq!(cfg.l1_ws_provider_count, 0);
1301 assert_eq!(cfg.storage.backend, StorageBackend::FsDefault);
1302 assert!(cfg.storage.fs.is_none());
1303 assert!(cfg.storage.sql.is_none());
1304 assert!(!cfg.modules.submit);
1305 assert!(cfg.modules.http.is_none());
1306 assert!(cfg.modules.query.is_none());
1307
1308 let value: serde_json::Value = serde_json::to_value(&cfg).unwrap();
1309 assert_eq!(
1310 value["cliquenet_advertise_address"],
1311 serde_json::Value::Null
1312 );
1313 assert_eq!(value["libp2p_bootstrap_nodes"], serde_json::Value::Null);
1314 assert_eq!(value["config_peers"], serde_json::Value::Null);
1315 assert_eq!(value["public_api_url"], serde_json::Value::Null);
1316 }
1317
1318 #[cfg(not(feature = "embedded-db"))]
1322 #[test]
1323 fn config_node_response_snapshot() {
1324 let opt = parse_options_with(&[
1325 "--orchestrator-url",
1326 "http://orchestrator.example:8080",
1327 "--cdn-endpoint",
1328 "cdn.example:8081",
1329 "--cliquenet-bind-address",
1330 "0.0.0.0:9977",
1331 "--cliquenet-advertise-address",
1332 "node1.example:9977",
1333 "--libp2p-bind-address",
1334 "0.0.0.0:1769",
1335 "--libp2p-advertise-address",
1336 "node1.example:1769",
1337 "--libp2p-bootstrap-nodes",
1338 "/ip4/10.0.0.1/tcp/1769",
1339 "--public-api-url",
1340 "http://node1.example:24000",
1341 "--builder-urls",
1342 "http://builder.example:31004",
1343 "--state-relay-server-url",
1344 "http://relay.example:8083",
1345 "--state-peers",
1346 "https://peer1.example,https://peer2.example",
1347 "--config-peers",
1348 "https://peer1.example",
1349 "--is-da",
1350 "--genesis-file",
1351 "/path/to/genesis.toml",
1352 "--l1-provider-url",
1353 "https://eth.example",
1354 "--country-code",
1355 "US",
1356 "--node-name",
1357 "Snapshot Node",
1358 "--operating-system",
1360 "linux",
1361 "--node-type",
1362 "espresso-sequencer 0.0.0",
1363 "--",
1364 "storage-sql",
1365 "--prune",
1366 "--pruning-threshold",
1367 "1000000000000",
1368 "--",
1369 "http",
1370 "--port",
1371 "24000",
1372 "--",
1373 "query",
1374 "--",
1375 "config",
1376 ]);
1377 let modules = opt.modules();
1378
1379 let cfg = PublicNodeConfig::new(&opt, &modules, &test_genesis());
1380
1381 insta::assert_yaml_snapshot!("config_node_response_postgres", cfg);
1382 }
1383
1384 #[cfg(not(feature = "embedded-db"))]
1387 #[test]
1388 fn public_node_config_includes_pruning() {
1389 let opt = parse_options_with(&[
1390 "--cliquenet-bind-address",
1391 "127.0.0.1:1",
1392 "--",
1393 "storage-sql",
1394 "--prune",
1395 "--pruning-threshold",
1396 "1000000000000",
1397 ]);
1398 let modules = opt.modules();
1399
1400 let cfg = PublicNodeConfig::new(&opt, &modules, &test_genesis());
1401 let json = serde_json::to_string(&cfg).unwrap();
1402
1403 assert_eq!(cfg.storage.backend, StorageBackend::Sql);
1404 assert!(
1405 json.contains("\"prune\":true"),
1406 "expected prune:true in JSON: {json}"
1407 );
1408 assert!(
1409 json.contains("pruning_threshold"),
1410 "expected pruning_threshold in JSON: {json}"
1411 );
1412 assert!(
1413 json.contains("1000000000000"),
1414 "expected pruning threshold value in JSON: {json}"
1415 );
1416 assert!(
1417 json.contains("\"consensus_pruning\""),
1418 "expected consensus_pruning object in JSON: {json}"
1419 );
1420 assert!(
1421 json.contains("\"pruning\""),
1422 "expected pruning object in JSON: {json}"
1423 );
1424 assert!(
1425 json.contains("\"target_retention\":302000"),
1426 "expected consensus_pruning target_retention default in JSON: {json}"
1427 );
1428 assert!(
1429 json.contains("\"minimum_retention\":130000"),
1430 "expected consensus_pruning minimum_retention default in JSON: {json}"
1431 );
1432 assert!(
1433 json.contains("\"target_usage\":1000000000"),
1434 "expected consensus_pruning target_usage default in JSON: {json}"
1435 );
1436 assert!(
1437 !json.contains("\"uri\""),
1438 "DB URI key leaked into JSON: {json}"
1439 );
1440 assert!(
1441 !json.contains("postgres://"),
1442 "DB connection string leaked into JSON: {json}"
1443 );
1444 }
1445}