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