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