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 num::NonZeroU64,
10 path::PathBuf,
11 time::Duration,
12};
13
14use clap::{Args, FromArgMatches, Parser, error::ErrorKind};
15use derivative::Derivative;
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::{api, keyset::KeySetOptions, persistence, proposal_fetcher::ProposalFetcherConfig};
25
26#[derive(Parser, Clone, Derivative)]
46#[derivative(Debug(bound = ""))]
47#[command(version = build_version())]
48pub struct Options {
49 #[clap(
51 short,
52 long,
53 env = "ESPRESSO_NODE_ORCHESTRATOR_URL",
54 default_value = "http://localhost:8080"
55 )]
56 #[derivative(Debug(format_with = "Display::fmt"))]
57 pub orchestrator_url: Url,
58
59 #[clap(
62 short,
63 long,
64 env = "ESPRESSO_NODE_CDN_ENDPOINT",
65 default_value = "127.0.0.1:8081"
66 )]
67 pub cdn_endpoint: String,
68
69 #[clap(
71 long,
72 env = "ESPRESSO_NODE_CLIQUENET_BIND_ADDRESS",
73 default_value = "0.0.0.0:9977"
74 )]
75 pub cliquenet_bind_address: NetAddr,
76
77 #[clap(long, env = "ESPRESSO_NODE_CLIQUENET_ADVERTISE_ADDRESS")]
82 pub cliquenet_advertise_address: Option<NetAddr>,
83
84 #[clap(
86 long,
87 env = "ESPRESSO_NODE_LIBP2P_BIND_ADDRESS",
88 default_value = "0.0.0.0:1769"
89 )]
90 pub libp2p_bind_address: String,
91
92 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_HEARTBEAT_INTERVAL", default_value = "1s", value_parser = parse_duration)]
94 pub libp2p_heartbeat_interval: Duration,
95
96 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_HISTORY_GOSSIP", default_value = "3")]
98 pub libp2p_history_gossip: usize,
99
100 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_HISTORY_LENGTH", default_value = "5")]
102 pub libp2p_history_length: usize,
103
104 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_MESH_N", default_value = "8")]
106 pub libp2p_mesh_n: usize,
107
108 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_MESH_N_HIGH", default_value = "12")]
110 pub libp2p_mesh_n_high: usize,
111
112 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_MESH_N_LOW", default_value = "6")]
114 pub libp2p_mesh_n_low: usize,
115
116 #[clap(
118 long,
119 env = "ESPRESSO_NODE_LIBP2P_MESH_OUTBOUND_MIN",
120 default_value = "2"
121 )]
122 pub libp2p_mesh_outbound_min: usize,
123
124 #[clap(
126 long,
127 env = "ESPRESSO_NODE_LIBP2P_MAX_IHAVE_LENGTH",
128 default_value = "5000"
129 )]
130 pub libp2p_max_ihave_length: usize,
131
132 #[clap(
134 long,
135 env = "ESPRESSO_NODE_LIBP2P_MAX_IHAVE_MESSAGES",
136 default_value = "10"
137 )]
138 pub libp2p_max_ihave_messages: usize,
139
140 #[clap(long, env = "ESPRESSO_NODE_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_NODE_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_NODE_LIBP2P_MAX_MESSAGES_PER_RPC")]
154 pub libp2p_max_messages_per_rpc: Option<usize>,
155
156 #[clap(
158 long,
159 env = "ESPRESSO_NODE_LIBP2P_GOSSIP_RETRANSMISSION",
160 default_value = "3"
161 )]
162 pub libp2p_gossip_retransmission: u32,
163
164 #[clap(
166 long,
167 env = "ESPRESSO_NODE_LIBP2P_FLOOD_PUBLISH",
168 default_value = "true"
169 )]
170 pub libp2p_flood_publish: bool,
171
172 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_DUPLICATE_CACHE_TIME", default_value = "20m", value_parser = parse_duration)]
174 pub libp2p_duplicate_cache_time: Duration,
175
176 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_FANOUT_TTL", default_value = "60s", value_parser = parse_duration)]
178 pub libp2p_fanout_ttl: Duration,
179
180 #[clap(long, env = "ESPRESSO_NODE_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_NODE_LIBP2P_GOSSIP_FACTOR",
188 default_value = "0.25"
189 )]
190 pub libp2p_gossip_factor: f64,
191
192 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_GOSSIP_LAZY", default_value = "6")]
194 pub libp2p_gossip_lazy: usize,
195
196 #[clap(
198 long,
199 env = "ESPRESSO_NODE_LIBP2P_MAX_GOSSIP_TRANSMIT_SIZE",
200 default_value = "2000000"
201 )]
202 pub libp2p_max_gossip_transmit_size: usize,
203
204 #[clap(
206 long,
207 env = "ESPRESSO_NODE_LIBP2P_MAX_DIRECT_TRANSMIT_SIZE",
208 default_value = "20000000"
209 )]
210 pub libp2p_max_direct_transmit_size: u64,
211
212 #[clap(long, env = "ESPRESSO_NODE_PUBLIC_API_URL")]
215 pub public_api_url: Option<Url>,
216
217 #[clap(long, env = "ESPRESSO_NODE_LIBP2P_ADVERTISE_ADDRESS")]
230 pub libp2p_advertise_address: Option<String>,
231
232 #[clap(
237 long,
238 env = "ESPRESSO_NODE_LIBP2P_BOOTSTRAP_NODES",
239 value_delimiter = ',',
240 num_args = 1..
241 )]
242 pub libp2p_bootstrap_nodes: Option<Vec<Multiaddr>>,
243
244 #[clap(long, env = "ESPRESSO_BUILDER_URLS", value_delimiter = ',')]
246 pub builder_urls: Vec<Url>,
247
248 #[clap(
250 long,
251 env = "ESPRESSO_STATE_RELAY_SERVER_URL",
252 default_value = "http://localhost:8083"
253 )]
254 #[derivative(Debug(format_with = "Display::fmt"))]
255 pub state_relay_server_url: Url,
256
257 #[clap(
259 long,
260 name = "GENESIS_FILE",
261 env = "ESPRESSO_NODE_GENESIS_FILE",
262 default_value = "/genesis/demo.toml"
263 )]
264 pub genesis_file: PathBuf,
265
266 #[clap(flatten)]
267 pub key_set: KeySetOptions,
268
269 #[clap(raw = true)]
283 modules: Vec<String>,
284
285 #[clap(
287 long,
288 env = "ESPRESSO_L1_PROVIDER",
289 default_value = "http://localhost:8545",
290 value_delimiter = ',',
291 num_args = 1..,
292 )]
293 #[derivative(Debug = "ignore")]
294 pub l1_provider_url: Vec<Url>,
295
296 #[clap(flatten)]
298 pub l1_options: L1ClientOptions,
299
300 #[clap(long, env = "ESPRESSO_NODE_IS_DA", action)]
302 pub is_da: bool,
303
304 #[clap(long, env = "ESPRESSO_NODE_STATE_PEERS", value_delimiter = ',')]
306 #[derivative(Debug(format_with = "fmt_urls"))]
307 pub state_peers: Vec<Url>,
308
309 #[clap(long, env = "ESPRESSO_NODE_CONFIG_PEERS", value_delimiter = ',')]
316 #[derivative(Debug(format_with = "fmt_opt_urls"))]
317 pub config_peers: Option<Vec<Url>>,
318
319 #[clap(flatten)]
321 pub catchup_backoff: BackoffParams,
322
323 #[clap(long, env = "ESPRESSO_NODE_CATCHUP_BASE_TIMEOUT", default_value = "2s", value_parser = parse_duration)]
327 pub catchup_base_timeout: Duration,
328
329 #[clap(long, env = "ESPRESSO_NODE_LOCAL_CATCHUP_TIMEOUT", default_value = "5s", value_parser = parse_duration)]
334 pub local_catchup_timeout: Duration,
335
336 #[clap(long, env = "ESPRESSO_NODE_BOOTSTRAP_EPOCH_CATCHUP_TIMEOUT", default_value = "30s", value_parser = parse_duration)]
342 pub bootstrap_epoch_catchup_timeout: Duration,
343
344 #[clap(
350 long,
351 env = "ESPRESSO_NODE_NEW_PROTOCOL_CONSENSUS_GC_INTERVAL",
352 default_value = "100"
353 )]
354 pub new_protocol_consensus_gc_interval: NonZeroU64,
355
356 #[clap(flatten)]
357 pub logging: logging::Config,
358
359 #[clap(flatten)]
360 pub identity: Identity,
361
362 #[clap(flatten)]
363 pub proposal_fetcher_config: ProposalFetcherConfig,
364}
365
366impl Options {
367 pub fn modules(&self) -> Modules {
368 ModuleArgs(self.modules.clone()).parse()
369 }
370}
371
372#[derive(Parser, Clone, Derivative, Serialize)]
377#[derivative(Debug(bound = ""))]
378pub struct Identity {
379 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_COUNTRY_CODE")]
380 pub country_code: Option<String>,
381 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_LATITUDE")]
382 pub latitude: Option<f64>,
383 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_LONGITUDE")]
384 pub longitude: Option<f64>,
385
386 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_NODE_NAME")]
387 pub node_name: Option<String>,
388 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_NODE_DESCRIPTION")]
389 pub node_description: Option<String>,
390
391 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_COMPANY_NAME")]
392 pub company_name: Option<String>,
393 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_COMPANY_WEBSITE")]
394 pub company_website: Option<Url>,
395 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_OPERATING_SYSTEM", default_value = std::env::consts::OS)]
396 pub operating_system: Option<String>,
397 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_NODE_TYPE", default_value = get_default_node_type())]
398 pub node_type: Option<String>,
399 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_NETWORK_TYPE")]
400 pub network_type: Option<String>,
401
402 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_14x14_1x")]
403 pub icon_14x14_1x: Option<Url>,
404 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_14x14_2x")]
405 pub icon_14x14_2x: Option<Url>,
406 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_14x14_3x")]
407 pub icon_14x14_3x: Option<Url>,
408 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_24x24_1x")]
409 pub icon_24x24_1x: Option<Url>,
410 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_24x24_2x")]
411 pub icon_24x24_2x: Option<Url>,
412 #[clap(long, env = "ESPRESSO_NODE_IDENTITY_ICON_24x24_3x")]
413 pub icon_24x24_3x: Option<Url>,
414}
415
416fn get_default_node_type() -> String {
419 format!("espresso-sequencer {}", env!("CARGO_PKG_VERSION"))
420}
421
422fn build_version() -> String {
423 let info = espresso_utils::build_info!();
424 format!(
425 "{}\nfeatures: {}",
426 info.clap_version(),
427 env!("VERGEN_CARGO_FEATURES"),
428 )
429}
430
431fn fmt_urls(v: &[Url], fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
433 write!(
434 fmt,
435 "{:?}",
436 v.iter().map(|i| i.to_string()).collect::<Vec<_>>()
437 )
438}
439
440fn fmt_opt_urls(
441 v: &Option<Vec<Url>>,
442 fmt: &mut std::fmt::Formatter,
443) -> Result<(), std::fmt::Error> {
444 match v {
445 Some(urls) => {
446 write!(fmt, "Some(")?;
447 fmt_urls(urls, fmt)?;
448 write!(fmt, ")")?;
449 },
450 None => {
451 write!(fmt, "None")?;
452 },
453 }
454 Ok(())
455}
456
457#[derive(Clone, Copy, Debug, PartialEq, Eq)]
458pub struct Ratio {
459 pub numerator: u64,
460 pub denominator: u64,
461}
462
463impl From<Ratio> for (u64, u64) {
464 fn from(r: Ratio) -> Self {
465 (r.numerator, r.denominator)
466 }
467}
468
469impl Display for Ratio {
470 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
471 write!(f, "{}:{}", self.numerator, self.denominator)
472 }
473}
474
475impl PartialOrd for Ratio {
476 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
477 Some(self.cmp(other))
478 }
479}
480
481impl Ord for Ratio {
482 fn cmp(&self, other: &Self) -> Ordering {
483 (self.numerator * other.denominator).cmp(&(other.numerator * self.denominator))
484 }
485}
486
487#[derive(Clone, Debug)]
488struct ModuleArgs(Vec<String>);
489
490impl ModuleArgs {
491 fn parse(&self) -> Modules {
492 match self.try_parse() {
493 Ok(modules) => modules,
494 Err(err) => err.exit(),
495 }
496 }
497
498 fn try_parse(&self) -> Result<Modules, clap::Error> {
499 let mut modules = Modules::default();
500 let mut curr = self.0.clone();
501 let mut provided = Default::default();
502
503 while !curr.is_empty() {
504 let module = SequencerModule::try_parse_from(
508 once("sequencer --").chain(curr.iter().map(|s| s.as_str())),
509 )?;
510 match module {
511 SequencerModule::Storage(m) => {
512 curr = m.add(&mut modules.storage_fs, &mut provided)?
513 },
514 SequencerModule::StorageFs(m) => {
515 curr = m.add(&mut modules.storage_fs, &mut provided)?
516 },
517 SequencerModule::StorageSql(m) => {
518 curr = m.add(&mut modules.storage_sql, &mut provided)?
519 },
520 SequencerModule::Http(m) => curr = m.add(&mut modules.http, &mut provided)?,
521 SequencerModule::Query(m) => curr = m.add(&mut modules.query, &mut provided)?,
522 SequencerModule::Submit(m) => curr = m.add(&mut modules.submit, &mut provided)?,
523 SequencerModule::Status(m) => curr = m.add(&mut modules.status, &mut provided)?,
524 SequencerModule::Catchup(m) => curr = m.add(&mut modules.catchup, &mut provided)?,
525 SequencerModule::Config(m) => curr = m.add(&mut modules.config, &mut provided)?,
526 SequencerModule::HotshotEvents(m) => {
527 curr = m.add(&mut modules.hotshot_events, &mut provided)?
528 },
529 SequencerModule::Explorer(m) => {
530 curr = m.add(&mut modules.explorer, &mut provided)?
531 },
532 SequencerModule::LightClient(m) => {
533 curr = m.add(&mut modules.light_client, &mut provided)?
534 },
535 }
536 }
537
538 Ok(modules)
539 }
540}
541
542trait ModuleInfo: Args + FromArgMatches {
543 const NAME: &'static str;
544 fn requires() -> Vec<&'static str>;
545}
546
547macro_rules! module {
548 ($name:expr, $opt:ty $(,requires: $($req:expr),*)?) => {
549 impl ModuleInfo for $opt {
550 const NAME: &'static str = $name;
551
552 fn requires() -> Vec<&'static str> {
553 vec![$($($req),*)?]
554 }
555 }
556 };
557}
558
559module!("storage-fs", persistence::fs::Options);
560module!("storage-sql", persistence::sql::Options);
561module!("http", api::options::Http);
562module!("query", api::options::Query, requires: "http");
563module!("submit", api::options::Submit, requires: "http");
564module!("status", api::options::Status, requires: "http");
565module!("catchup", api::options::Catchup, requires: "http");
566module!("config", api::options::Config, requires: "http");
567module!("hotshot-events", api::options::HotshotEvents, requires: "http");
568module!("explorer", api::options::Explorer, requires: "http", "storage-sql");
569module!("light-client", api::options::LightClient, requires: "http", "storage-sql");
570
571#[derive(Clone, Debug, Args)]
572struct Module<Options: ModuleInfo> {
573 #[clap(flatten)]
574 options: Box<Options>,
575
576 #[clap(raw = true)]
578 modules: Vec<String>,
579}
580
581impl<Options: ModuleInfo> Module<Options> {
582 fn add(
584 self,
585 options: &mut Option<Options>,
586 provided: &mut HashSet<&'static str>,
587 ) -> Result<Vec<String>, clap::Error> {
588 if options.is_some() {
589 return Err(clap::Error::raw(
590 ErrorKind::TooManyValues,
591 format!("optional module {} can only be started once", Options::NAME),
592 ));
593 }
594 for req in Options::requires() {
595 if !provided.contains(&req) {
596 return Err(clap::Error::raw(
597 ErrorKind::MissingRequiredArgument,
598 format!("module {} is missing required module {req}", Options::NAME),
599 ));
600 }
601 }
602 *options = Some(*self.options);
603 provided.insert(Options::NAME);
604 Ok(self.modules)
605 }
606}
607
608#[derive(Clone, Debug, Parser)]
609enum SequencerModule {
610 Http(Module<api::options::Http>),
617 Storage(Module<persistence::fs::Options>),
619 StorageFs(Module<persistence::fs::Options>),
621 StorageSql(Module<persistence::sql::Options>),
623 Query(Module<api::options::Query>),
627 Submit(Module<api::options::Submit>),
631 Status(Module<api::options::Status>),
635 Catchup(Module<api::options::Catchup>),
639 Config(Module<api::options::Config>),
641
642 HotshotEvents(Module<api::options::HotshotEvents>),
646 Explorer(Module<api::options::Explorer>),
650 LightClient(Module<api::options::LightClient>),
657}
658
659#[derive(Clone, Debug, Default)]
660pub struct Modules {
661 pub storage_fs: Option<persistence::fs::Options>,
662 pub storage_sql: Option<persistence::sql::Options>,
663 pub http: Option<api::options::Http>,
664 pub query: Option<api::options::Query>,
665 pub submit: Option<api::options::Submit>,
666 pub status: Option<api::options::Status>,
667 pub catchup: Option<api::options::Catchup>,
668 pub config: Option<api::options::Config>,
669 pub hotshot_events: Option<api::options::HotshotEvents>,
670 pub explorer: Option<api::options::Explorer>,
671 pub light_client: Option<api::options::LightClient>,
672}
673
674#[derive(Clone, Debug, Serialize)]
675pub struct PublicNodeConfig {
676 pub orchestrator_url: Url,
677 pub cdn_endpoint: String,
678 pub cliquenet_bind_address: NetAddr,
679 pub cliquenet_advertise_address: Option<NetAddr>,
680 pub libp2p_bind_address: String,
681 pub libp2p_advertise_address: Option<String>,
682 pub libp2p_bootstrap_nodes: Option<Vec<Multiaddr>>,
683 pub public_api_url: Option<Url>,
684 pub builder_urls: Vec<Url>,
685 pub state_relay_server_url: Url,
686 pub state_peers: Vec<Url>,
687 pub config_peers: Option<Vec<Url>>,
688 pub is_da: bool,
689 pub genesis_file: PathBuf,
690 pub identity: Identity,
691 pub catchup_base_timeout: Duration,
692 pub local_catchup_timeout: Duration,
693 pub bootstrap_epoch_catchup_timeout: Duration,
694 pub new_protocol_consensus_gc_interval: NonZeroU64,
695 pub catchup_backoff: BackoffParams,
696 pub proposal_fetcher: ProposalFetcherConfig,
697 pub libp2p: Libp2pTuning,
698 pub l1: L1Tuning,
699 pub l1_provider_count: usize,
700 pub l1_ws_provider_count: usize,
701 pub storage: StorageConfig,
702 pub modules: ApiModulesConfig,
703}
704
705#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
706#[serde(rename_all = "kebab-case")]
707pub enum StorageBackend {
708 Sql,
709 Fs,
710 FsDefault,
711}
712
713#[derive(Clone, Debug, Serialize)]
714pub struct StorageConfig {
715 pub backend: StorageBackend,
717 pub fs: Option<FsStorageConfig>,
718 pub sql: Option<SqlStorageConfig>,
719}
720
721#[derive(Clone, Debug, Serialize)]
722pub struct FsStorageConfig {
723 pub path: PathBuf,
724 pub consensus_view_retention: u64,
725}
726
727#[derive(Clone, Debug, Serialize)]
728pub struct SqlStorageConfig {
729 pub prune: bool,
730 pub archive: bool,
731 pub lightweight: bool,
732 pub disable_proactive_fetching: bool,
733 pub fetch_rate_limit: Option<usize>,
734 pub active_fetch_delay: Option<Duration>,
735 pub chunk_fetch_delay: Option<Duration>,
736 pub sync_status_chunk_size: Option<usize>,
737 pub sync_status_ttl: Option<Duration>,
738 pub proactive_scan_chunk_size: Option<usize>,
739 pub proactive_scan_interval: Option<Duration>,
740 pub idle_connection_timeout: Duration,
741 pub connection_timeout: Duration,
742 pub slow_statement_threshold: Duration,
743 pub statement_timeout: Duration,
744 pub min_connections: u32,
745 pub max_connections: u32,
746 pub query_min_connections: Option<u32>,
747 pub query_max_connections: Option<u32>,
748 pub pruning: PruningView,
749 pub consensus_pruning: ConsensusPruningView,
750}
751
752#[derive(Clone, Debug, Serialize)]
753pub struct PruningView {
754 pub pruning_threshold: Option<u64>,
755 pub minimum_retention: Option<Duration>,
756 pub target_retention: Option<Duration>,
757 pub batch_size: Option<u64>,
758 pub max_usage: Option<u16>,
759 pub interval: Option<Duration>,
760 pub pages: Option<u64>,
761}
762
763#[derive(Clone, Debug, Serialize)]
764pub struct ConsensusPruningView {
765 pub target_retention: u64,
766 pub minimum_retention: u64,
767 pub target_usage: u64,
768}
769
770#[derive(Clone, Debug, Serialize)]
771pub struct ApiModulesConfig {
772 pub http: Option<HttpConfig>,
773 pub query: Option<QueryConfig>,
774 pub submit: bool,
775 pub status: bool,
776 pub catchup: bool,
777 pub config: bool,
778 pub hotshot_events: bool,
779 pub explorer: bool,
780 pub light_client: bool,
781}
782
783#[derive(Clone, Debug, Serialize)]
784pub struct HttpConfig {
785 pub port: u16,
786 pub max_connections: Option<usize>,
787 pub axum_port: Option<u16>,
788 pub tonic_port: Option<u16>,
789}
790
791#[derive(Clone, Debug, Serialize)]
792pub struct QueryConfig {
793 pub peers: Vec<Url>,
794 pub light_client: LightClientOptions,
795 pub light_client_db: LightClientSqliteOptions,
796}
797
798impl From<&persistence::sql::PruningOptions> for PruningView {
799 fn from(o: &persistence::sql::PruningOptions) -> Self {
800 Self {
801 pruning_threshold: o.pruning_threshold,
802 minimum_retention: o.minimum_retention,
803 target_retention: o.target_retention,
804 batch_size: o.batch_size,
805 max_usage: o.max_usage,
806 interval: o.interval,
807 pages: o.pages,
808 }
809 }
810}
811
812impl From<&persistence::sql::ConsensusPruningOptions> for ConsensusPruningView {
813 fn from(o: &persistence::sql::ConsensusPruningOptions) -> Self {
814 Self {
815 target_retention: o.target_retention,
816 minimum_retention: o.minimum_retention,
817 target_usage: o.target_usage,
818 }
819 }
820}
821
822impl From<&persistence::sql::Options> for SqlStorageConfig {
823 fn from(o: &persistence::sql::Options) -> Self {
824 Self {
825 prune: o.prune,
826 archive: o.archive,
827 lightweight: o.lightweight,
828 disable_proactive_fetching: o.disable_proactive_fetching,
829 fetch_rate_limit: o.fetch_rate_limit,
830 active_fetch_delay: o.active_fetch_delay,
831 chunk_fetch_delay: o.chunk_fetch_delay,
832 sync_status_chunk_size: o.sync_status_chunk_size,
833 sync_status_ttl: o.sync_status_ttl,
834 proactive_scan_chunk_size: o.proactive_scan_chunk_size,
835 proactive_scan_interval: o.proactive_scan_interval,
836 idle_connection_timeout: o.idle_connection_timeout,
837 connection_timeout: o.connection_timeout,
838 slow_statement_threshold: o.slow_statement_threshold,
839 statement_timeout: o.statement_timeout,
840 min_connections: o.min_connections,
841 max_connections: o.max_connections,
842 #[cfg(not(feature = "embedded-db"))]
843 query_min_connections: o.query_min_connections,
844 #[cfg(feature = "embedded-db")]
845 query_min_connections: None,
846 #[cfg(not(feature = "embedded-db"))]
847 query_max_connections: o.query_max_connections,
848 #[cfg(feature = "embedded-db")]
849 query_max_connections: None,
850 pruning: PruningView::from(&o.pruning),
851 consensus_pruning: ConsensusPruningView::from(&o.consensus_pruning),
852 }
853 }
854}
855
856impl From<&persistence::fs::Options> for FsStorageConfig {
857 fn from(o: &persistence::fs::Options) -> Self {
858 Self {
859 path: o.path.clone(),
860 consensus_view_retention: o.consensus_view_retention,
861 }
862 }
863}
864
865impl From<&api::options::Http> for HttpConfig {
866 fn from(o: &api::options::Http) -> Self {
867 Self {
868 port: o.port,
869 max_connections: o.max_connections,
870 axum_port: o.axum_port,
871 tonic_port: o.tonic_port,
872 }
873 }
874}
875
876impl From<&api::options::Query> for QueryConfig {
877 fn from(o: &api::options::Query) -> Self {
878 Self {
879 peers: o.peers.clone(),
880 light_client: o.light_client.clone(),
881 light_client_db: o.light_client_db.clone(),
882 }
883 }
884}
885
886impl From<&Modules> for ApiModulesConfig {
887 fn from(m: &Modules) -> Self {
888 Self {
889 http: m.http.as_ref().map(HttpConfig::from),
890 query: m.query.as_ref().map(QueryConfig::from),
891 submit: m.submit.is_some(),
892 status: m.status.is_some(),
893 catchup: m.catchup.is_some(),
894 config: m.config.is_some(),
895 hotshot_events: m.hotshot_events.is_some(),
896 explorer: m.explorer.is_some(),
897 light_client: m.light_client.is_some(),
898 }
899 }
900}
901
902#[derive(Clone, Debug, Serialize)]
903pub struct Libp2pTuning {
904 pub heartbeat_interval: Duration,
905 pub heartbeat_initial_delay: Duration,
906 pub history_gossip: usize,
907 pub history_length: usize,
908 pub mesh_n: usize,
909 pub mesh_n_high: usize,
910 pub mesh_n_low: usize,
911 pub mesh_outbound_min: usize,
912 pub max_ihave_length: usize,
913 pub max_ihave_messages: usize,
914 pub published_message_ids_cache_time: Duration,
915 pub iwant_followup_time: Duration,
916 pub max_messages_per_rpc: Option<usize>,
917 pub gossip_retransmission: u32,
918 pub gossip_factor: f64,
919 pub gossip_lazy: usize,
920 pub max_gossip_transmit_size: usize,
921 pub max_direct_transmit_size: u64,
922 pub fanout_ttl: Duration,
923 pub duplicate_cache_time: Duration,
924 pub flood_publish: bool,
925}
926
927#[derive(Clone, Debug, Serialize)]
928pub struct L1Tuning {
929 pub retry_delay: Duration,
930 pub polling_interval: Duration,
931 pub blocks_cache_size: usize,
932 pub events_channel_capacity: usize,
933 pub events_max_block_range: u64,
934 pub subscription_timeout: Duration,
935 pub frequent_failure_tolerance: Duration,
936 pub consecutive_failure_tolerance: usize,
937 pub failover_revert: Duration,
938 pub rate_limit_delay: Option<Duration>,
939 pub stake_table_update_interval: Duration,
940 pub events_max_retry_duration: Duration,
941 pub finalized_safety_margin: Option<u64>,
942}
943
944impl From<&Options> for Libp2pTuning {
945 fn from(o: &Options) -> Self {
946 Self {
947 heartbeat_interval: o.libp2p_heartbeat_interval,
948 heartbeat_initial_delay: o.libp2p_heartbeat_initial_delay,
949 history_gossip: o.libp2p_history_gossip,
950 history_length: o.libp2p_history_length,
951 mesh_n: o.libp2p_mesh_n,
952 mesh_n_high: o.libp2p_mesh_n_high,
953 mesh_n_low: o.libp2p_mesh_n_low,
954 mesh_outbound_min: o.libp2p_mesh_outbound_min,
955 max_ihave_length: o.libp2p_max_ihave_length,
956 max_ihave_messages: o.libp2p_max_ihave_messages,
957 published_message_ids_cache_time: o.libp2p_published_message_ids_cache_time,
958 iwant_followup_time: o.libp2p_iwant_followup_time,
959 max_messages_per_rpc: o.libp2p_max_messages_per_rpc,
960 gossip_retransmission: o.libp2p_gossip_retransmission,
961 gossip_factor: o.libp2p_gossip_factor,
962 gossip_lazy: o.libp2p_gossip_lazy,
963 max_gossip_transmit_size: o.libp2p_max_gossip_transmit_size,
964 max_direct_transmit_size: o.libp2p_max_direct_transmit_size,
965 fanout_ttl: o.libp2p_fanout_ttl,
966 duplicate_cache_time: o.libp2p_duplicate_cache_time,
967 flood_publish: o.libp2p_flood_publish,
968 }
969 }
970}
971
972impl From<&L1ClientOptions> for L1Tuning {
973 fn from(o: &L1ClientOptions) -> Self {
974 Self {
975 retry_delay: o.l1_retry_delay,
976 polling_interval: o.l1_polling_interval,
977 blocks_cache_size: o.l1_blocks_cache_size.get(),
978 events_channel_capacity: o.l1_events_channel_capacity,
979 events_max_block_range: o.l1_events_max_block_range,
980 subscription_timeout: o.subscription_timeout,
981 frequent_failure_tolerance: o.l1_frequent_failure_tolerance,
982 consecutive_failure_tolerance: o.l1_consecutive_failure_tolerance,
983 failover_revert: o.l1_failover_revert,
984 rate_limit_delay: o.l1_rate_limit_delay,
985 stake_table_update_interval: o.stake_table_update_interval,
986 events_max_retry_duration: o.l1_events_max_retry_duration,
987 finalized_safety_margin: o.l1_finalized_safety_margin,
988 }
989 }
990}
991
992impl PublicNodeConfig {
993 pub fn new(opt: &Options, modules: &Modules) -> Self {
994 let storage = if let Some(sql) = modules.storage_sql.as_ref() {
995 StorageConfig {
996 backend: StorageBackend::Sql,
997 fs: None,
998 sql: Some(SqlStorageConfig::from(sql)),
999 }
1000 } else if let Some(fs) = modules.storage_fs.as_ref() {
1001 StorageConfig {
1002 backend: StorageBackend::Fs,
1003 fs: Some(FsStorageConfig::from(fs)),
1004 sql: None,
1005 }
1006 } else {
1007 let fs = persistence::fs::Options::try_parse_from(std::iter::empty::<String>()).ok();
1008 StorageConfig {
1009 backend: StorageBackend::FsDefault,
1010 fs: fs.as_ref().map(FsStorageConfig::from),
1011 sql: None,
1012 }
1013 };
1014
1015 Self {
1016 orchestrator_url: opt.orchestrator_url.clone(),
1017 cdn_endpoint: opt.cdn_endpoint.clone(),
1018 cliquenet_bind_address: opt.cliquenet_bind_address.clone(),
1019 cliquenet_advertise_address: opt.cliquenet_advertise_address.clone(),
1020 libp2p_bind_address: opt.libp2p_bind_address.clone(),
1021 libp2p_advertise_address: opt.libp2p_advertise_address.clone(),
1022 libp2p_bootstrap_nodes: opt.libp2p_bootstrap_nodes.clone(),
1023 public_api_url: opt.public_api_url.clone(),
1024 builder_urls: opt.builder_urls.clone(),
1025 state_relay_server_url: opt.state_relay_server_url.clone(),
1026 state_peers: opt.state_peers.clone(),
1027 config_peers: opt.config_peers.clone(),
1028 is_da: opt.is_da,
1029 genesis_file: opt.genesis_file.clone(),
1030 identity: opt.identity.clone(),
1031 catchup_base_timeout: opt.catchup_base_timeout,
1032 local_catchup_timeout: opt.local_catchup_timeout,
1033 bootstrap_epoch_catchup_timeout: opt.bootstrap_epoch_catchup_timeout,
1034 new_protocol_consensus_gc_interval: opt.new_protocol_consensus_gc_interval,
1035 catchup_backoff: opt.catchup_backoff,
1036 proposal_fetcher: opt.proposal_fetcher_config,
1037 libp2p: Libp2pTuning::from(opt),
1038 l1: L1Tuning::from(&opt.l1_options),
1039 l1_provider_count: opt.l1_provider_url.len(),
1040 l1_ws_provider_count: opt
1041 .l1_options
1042 .l1_ws_provider
1043 .as_ref()
1044 .map(|v| v.len())
1045 .unwrap_or(0),
1046 storage,
1047 modules: ApiModulesConfig::from(modules),
1048 }
1049 }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054 use espresso_types::PubKey;
1055 use hotshot_types::{light_client::StateKeyPair, traits::signature_key::SignatureKey, x25519};
1056 use tagged_base64::TaggedBase64;
1057
1058 use super::*;
1059
1060 #[test]
1061 fn test_build_version() {
1062 let version = build_version();
1063 for field in [
1064 "describe:",
1065 "rev:",
1066 "modified:",
1067 "branch:",
1068 "commit-timestamp:",
1069 "debug:",
1070 "os:",
1071 "arch:",
1072 "features:",
1073 ] {
1074 assert!(version.contains(field), "missing {field}: {version}");
1075 }
1076 assert!(
1077 version.contains("debug: true"),
1078 "expected debug build in test: {version}"
1079 );
1080 assert!(
1081 version.contains("testing"),
1082 "expected testing in features: {version}"
1083 );
1084 }
1085
1086 pub(super) fn parse_options_with(extra: &[&str]) -> Options {
1088 let (_, priv_key) = PubKey::generated_from_seed_indexed([0; 32], 0);
1089 let state_key = StateKeyPair::generate_from_seed_indexed([0; 32], 0);
1090 let x25519_kp = x25519::Keypair::generate().unwrap();
1091
1092 let priv_staking = priv_key.to_tagged_base64().expect("valid key").to_string();
1093 let priv_state = state_key
1094 .sign_key_ref()
1095 .to_tagged_base64()
1096 .expect("valid key")
1097 .to_string();
1098 let priv_x25519 = TaggedBase64::try_from(x25519_kp.secret_key())
1099 .expect("valid key")
1100 .to_string();
1101
1102 let mut args: Vec<String> = vec![
1103 "sequencer".into(),
1104 "--private-staking-key".into(),
1105 priv_staking,
1106 "--private-state-key".into(),
1107 priv_state,
1108 "--private-x25519-key".into(),
1109 priv_x25519,
1110 ];
1111 args.extend(extra.iter().map(|s| s.to_string()));
1112
1113 Options::parse_from(args)
1114 }
1115
1116 #[test]
1117 fn public_node_config_no_secrets() {
1118 let opt = parse_options_with(&[
1119 "--l1-provider-url",
1120 "https://user:pass@example.invalid/v2/SECRET_API_KEY,https://example2.invalid/key2",
1121 "--cliquenet-bind-address",
1122 "127.0.0.1:9999",
1123 "--state-peers",
1124 "https://peer1.test,https://peer2.test",
1125 ]);
1126 let modules = opt.modules();
1127
1128 let cfg = PublicNodeConfig::new(&opt, &modules);
1129 let json = serde_json::to_string(&cfg).unwrap();
1130 let json_lc = json.to_lowercase();
1131
1132 assert!(
1133 json.contains("127.0.0.1:9999"),
1134 "CLI override missing from JSON: {json}"
1135 );
1136 assert!(
1137 !json.contains("SECRET_API_KEY"),
1138 "L1 API key leaked into JSON: {json}"
1139 );
1140 assert!(
1141 !json.contains("user:pass"),
1142 "L1 URL userinfo leaked into JSON: {json}"
1143 );
1144 assert!(
1145 !json.contains("example.invalid"),
1146 "L1 host leaked into JSON: {json}"
1147 );
1148 assert!(
1149 !json.contains("example2.invalid"),
1150 "second L1 host leaked into JSON: {json}"
1151 );
1152 assert!(
1153 json.contains("\"l1_provider_count\":2"),
1154 "missing l1_provider_count: {json}"
1155 );
1156 assert!(
1157 !json.contains("\"uri\""),
1158 "DB URI key leaked into JSON: {json}"
1159 );
1160 assert!(
1161 !json.contains("postgres://"),
1162 "DB connection string leaked into JSON: {json}"
1163 );
1164
1165 const FORBIDDEN: &[&str] = &[
1166 "private", "mnemonic", "secret", "x25519", "key_file", "seed", "password",
1167 ];
1168 for token in FORBIDDEN {
1169 assert!(
1170 !json_lc.contains(token),
1171 "forbidden token '{token}' leaked into JSON: {json}"
1172 );
1173 }
1174
1175 assert!(
1176 json.contains("peer1.test") && json.contains("peer2.test"),
1177 "state_peers missing from JSON: {json}"
1178 );
1179 }
1180
1181 #[test]
1182 fn public_node_config_optionals() {
1183 let opt = parse_options_with(&[]);
1184 let modules = opt.modules();
1185
1186 let cfg = PublicNodeConfig::new(&opt, &modules);
1187
1188 assert!(
1189 cfg.cliquenet_advertise_address.is_none(),
1190 "expected no advertise address: {:?}",
1191 cfg.cliquenet_advertise_address
1192 );
1193 assert!(
1194 cfg.libp2p_bootstrap_nodes.is_none(),
1195 "expected no bootstrap nodes: {:?}",
1196 cfg.libp2p_bootstrap_nodes
1197 );
1198 assert!(
1199 cfg.config_peers.is_none(),
1200 "expected no config peers: {:?}",
1201 cfg.config_peers
1202 );
1203 assert_eq!(cfg.l1_ws_provider_count, 0);
1204 assert_eq!(cfg.storage.backend, StorageBackend::FsDefault);
1205 assert!(cfg.storage.fs.is_none());
1206 assert!(cfg.storage.sql.is_none());
1207 assert!(!cfg.modules.submit);
1208 assert!(cfg.modules.http.is_none());
1209 assert!(cfg.modules.query.is_none());
1210
1211 let value: serde_json::Value = serde_json::to_value(&cfg).unwrap();
1212 assert_eq!(
1213 value["cliquenet_advertise_address"],
1214 serde_json::Value::Null
1215 );
1216 assert_eq!(value["libp2p_bootstrap_nodes"], serde_json::Value::Null);
1217 assert_eq!(value["config_peers"], serde_json::Value::Null);
1218 assert_eq!(value["public_api_url"], serde_json::Value::Null);
1219 }
1220
1221 #[cfg(not(feature = "embedded-db"))]
1225 #[test]
1226 fn config_node_response_snapshot() {
1227 let opt = parse_options_with(&[
1228 "--orchestrator-url",
1229 "http://orchestrator.example:8080",
1230 "--cdn-endpoint",
1231 "cdn.example:8081",
1232 "--cliquenet-bind-address",
1233 "0.0.0.0:9977",
1234 "--cliquenet-advertise-address",
1235 "node1.example:9977",
1236 "--libp2p-bind-address",
1237 "0.0.0.0:1769",
1238 "--libp2p-advertise-address",
1239 "node1.example:1769",
1240 "--libp2p-bootstrap-nodes",
1241 "/ip4/10.0.0.1/tcp/1769",
1242 "--public-api-url",
1243 "http://node1.example:24000",
1244 "--builder-urls",
1245 "http://builder.example:31004",
1246 "--state-relay-server-url",
1247 "http://relay.example:8083",
1248 "--state-peers",
1249 "https://peer1.example,https://peer2.example",
1250 "--config-peers",
1251 "https://peer1.example",
1252 "--is-da",
1253 "--genesis-file",
1254 "/path/to/genesis.toml",
1255 "--l1-provider-url",
1256 "https://eth.example",
1257 "--country-code",
1258 "US",
1259 "--node-name",
1260 "Snapshot Node",
1261 "--operating-system",
1263 "linux",
1264 "--node-type",
1265 "espresso-sequencer 0.0.0",
1266 "--",
1267 "storage-sql",
1268 "--prune",
1269 "--pruning-threshold",
1270 "1000000000000",
1271 "--",
1272 "http",
1273 "--port",
1274 "24000",
1275 "--",
1276 "query",
1277 "--",
1278 "config",
1279 ]);
1280 let modules = opt.modules();
1281
1282 let cfg = PublicNodeConfig::new(&opt, &modules);
1283
1284 insta::assert_yaml_snapshot!("config_node_response_postgres", cfg);
1285 }
1286
1287 #[cfg(not(feature = "embedded-db"))]
1290 #[test]
1291 fn public_node_config_includes_pruning() {
1292 let opt = parse_options_with(&[
1293 "--cliquenet-bind-address",
1294 "127.0.0.1:1",
1295 "--",
1296 "storage-sql",
1297 "--prune",
1298 "--pruning-threshold",
1299 "1000000000000",
1300 ]);
1301 let modules = opt.modules();
1302
1303 let cfg = PublicNodeConfig::new(&opt, &modules);
1304 let json = serde_json::to_string(&cfg).unwrap();
1305
1306 assert_eq!(cfg.storage.backend, StorageBackend::Sql);
1307 assert!(
1308 json.contains("\"prune\":true"),
1309 "expected prune:true in JSON: {json}"
1310 );
1311 assert!(
1312 json.contains("pruning_threshold"),
1313 "expected pruning_threshold in JSON: {json}"
1314 );
1315 assert!(
1316 json.contains("1000000000000"),
1317 "expected pruning threshold value in JSON: {json}"
1318 );
1319 assert!(
1320 json.contains("\"consensus_pruning\""),
1321 "expected consensus_pruning object in JSON: {json}"
1322 );
1323 assert!(
1324 json.contains("\"pruning\""),
1325 "expected pruning object in JSON: {json}"
1326 );
1327 assert!(
1328 json.contains("\"target_retention\":302000"),
1329 "expected consensus_pruning target_retention default in JSON: {json}"
1330 );
1331 assert!(
1332 json.contains("\"minimum_retention\":130000"),
1333 "expected consensus_pruning minimum_retention default in JSON: {json}"
1334 );
1335 assert!(
1336 json.contains("\"target_usage\":1000000000"),
1337 "expected consensus_pruning target_usage default in JSON: {json}"
1338 );
1339 assert!(
1340 !json.contains("\"uri\""),
1341 "DB URI key leaked into JSON: {json}"
1342 );
1343 assert!(
1344 !json.contains("postgres://"),
1345 "DB connection string leaked into JSON: {json}"
1346 );
1347 }
1348}