1use clap::Parser;
2use espresso_telemetry as telemetry;
3use espresso_types::traits::NullEventConsumer;
4use futures::future::FutureExt;
5use hotshot_types::traits::metrics::NoMetrics;
6use url::Url;
7
8use super::{
9 Genesis, L1Params, NetworkParams,
10 api::{self, data_source::DataSourceOptions},
11 context::SequencerContext,
12 init_node, network,
13 options::{Modules, Options, PublicNodeConfig},
14 persistence,
15};
16use crate::{default_telemetry_endpoint, keyset::KeySet};
17
18pub async fn main(migrated_envs: Vec<(&str, &str)>) -> anyhow::Result<()> {
19 espresso_types::assert_node_feature();
20 let opt = Options::parse();
21
22 let genesis = Genesis::load(&opt.genesis_file).await?;
26 let telemetry_endpoint: Option<Url> = opt.telemetry.endpoint.clone().or_else(|| {
27 default_telemetry_endpoint(genesis.chain_config.chain_id).map(|s| {
28 s.parse()
29 .expect("default telemetry endpoint is a valid URL")
30 })
31 });
32 let telemetry_enabled = opt.telemetry.logs_enable || opt.telemetry.metrics_enable;
33
34 let (mut telemetry_handle, deferred_warnings, telemetry_init_error) =
38 match (opt.key_set.clone().try_into(), telemetry_endpoint.as_ref()) {
39 (Ok(KeySet { staking, .. }), Some(endpoint)) => match telemetry::init(
40 &opt.telemetry,
41 &staking,
42 opt.identity.node_name.as_deref(),
43 opt.identity.company_name.as_deref(),
44 endpoint,
45 telemetry::registry(),
46 ) {
47 Ok((h, warns)) => (h, warns, None),
48 Err(e) => (None, Vec::new(), Some(e.context("telemetry init failed"))),
49 },
50 (Ok(_), None) if telemetry_enabled => (
53 None,
54 vec![format!(
55 "telemetry enabled but no endpoint resolved for chain {}; set \
56 ESPRESSO_NODE_TELEMETRY_ENDPOINT",
57 genesis.chain_config.chain_id
58 )],
59 None,
60 ),
61 _ => (None, Vec::new(), None),
63 };
64 let otel_layer = telemetry_handle.as_ref().and_then(|h| h.tracing_layer());
65 opt.logging.init_with_otel(otel_layer);
66 for w in deferred_warnings {
67 tracing::warn!("{w}");
68 }
69 if let Some(e) = telemetry_init_error {
70 tracing::error!("{e:#}; continuing without telemetry");
71 }
72 espresso_utils::env_compat::log_migrated_env_vars(&migrated_envs);
73
74 let mut modules = opt.modules();
75 tracing::warn!(?modules, "sequencer starting up");
76
77 let public_node_config = PublicNodeConfig::new(&opt, &modules, &genesis);
78
79 tracing::warn!(?genesis, "genesis");
80
81 let result = if let Some(storage) = modules.storage_fs.take() {
82 run_with_storage(
83 genesis,
84 modules,
85 opt,
86 storage,
87 public_node_config,
88 telemetry_handle.as_mut(),
89 )
90 .await
91 } else if let Some(storage) = modules.storage_sql.take() {
92 run_with_storage(
93 genesis,
94 modules,
95 opt,
96 storage,
97 public_node_config,
98 telemetry_handle.as_mut(),
99 )
100 .await
101 } else {
102 run_with_storage(
104 genesis,
105 modules,
106 opt,
107 persistence::fs::Options::default(),
108 public_node_config,
109 telemetry_handle.as_mut(),
110 )
111 .await
112 };
113
114 if let Some(h) = telemetry_handle {
115 h.shutdown();
116 }
117 result
118}
119
120async fn run_with_storage<S>(
121 genesis: Genesis,
122 modules: Modules,
123 opt: Options,
124 storage_opt: S,
125 public_node_config: PublicNodeConfig,
126 telemetry_handle: Option<&mut telemetry::TelemetryHandle>,
127) -> anyhow::Result<()>
128where
129 S: DataSourceOptions,
130{
131 let mut ctx = init_with_storage(genesis, modules, opt, storage_opt, public_node_config).await?;
132
133 if let (Some(handle), Some(registry)) = (telemetry_handle, telemetry::registry()) {
137 handle.attach_metrics_push(registry);
138 }
139
140 ctx.start_consensus().await;
142
143 tokio::select! {
144 () = ctx.join() => tracing::warn!("consensus stopped; exiting"),
145 _ = espresso_utils::shutdown::wait_for_shutdown_signal() => ctx.shut_down().await,
146 }
147
148 Ok(())
149}
150
151pub async fn init_with_storage<S>(
152 genesis: Genesis,
153 modules: Modules,
154 opt: Options,
155 mut storage_opt: S,
156 public_node_config: PublicNodeConfig,
157) -> anyhow::Result<SequencerContext<network::Production, S::Persistence>>
158where
159 S: DataSourceOptions,
160{
161 let KeySet {
162 staking,
163 state,
164 x25519,
165 } = opt.key_set.try_into()?;
166 let l1_params = L1Params {
167 urls: opt.l1_provider_url,
168 options: opt.l1_options,
169 };
170
171 let network_params = NetworkParams {
172 cdn_endpoint: opt.cdn_endpoint,
173 cliquenet_bind_addr: opt.cliquenet_bind_address,
174 cliquenet_advertise_addr: opt.cliquenet_advertise_address,
175 x25519_secret_key: x25519,
176 libp2p_advertise_address: opt.libp2p_advertise_address,
177 libp2p_bind_address: opt.libp2p_bind_address,
178 libp2p_bootstrap_nodes: opt.libp2p_bootstrap_nodes,
179 orchestrator_url: opt.orchestrator_url,
180 builder_urls: opt.builder_urls,
181 state_relay_server_url: opt.state_relay_server_url,
182 public_api_url: opt.public_api_url,
183 private_staking_key: staking,
184 private_state_key: state,
185 state_peers: opt.state_peers,
186 config_peers: opt.config_peers,
187 catchup_backoff: opt.catchup_backoff,
188 catchup_base_timeout: opt.catchup_base_timeout,
189 local_catchup_timeout: opt.local_catchup_timeout,
190 bootstrap_epoch_catchup_timeout: opt.bootstrap_epoch_catchup_timeout,
191 libp2p_history_gossip: opt.libp2p_history_gossip,
192 libp2p_history_length: opt.libp2p_history_length,
193 libp2p_max_ihave_length: opt.libp2p_max_ihave_length,
194 libp2p_max_ihave_messages: opt.libp2p_max_ihave_messages,
195 libp2p_max_gossip_transmit_size: opt.libp2p_max_gossip_transmit_size,
196 libp2p_max_direct_transmit_size: opt.libp2p_max_direct_transmit_size,
197 libp2p_mesh_outbound_min: opt.libp2p_mesh_outbound_min,
198 libp2p_mesh_n: opt.libp2p_mesh_n,
199 libp2p_mesh_n_high: opt.libp2p_mesh_n_high,
200 libp2p_heartbeat_interval: opt.libp2p_heartbeat_interval,
201 libp2p_mesh_n_low: opt.libp2p_mesh_n_low,
202 libp2p_published_message_ids_cache_time: opt.libp2p_published_message_ids_cache_time,
203 libp2p_iwant_followup_time: opt.libp2p_iwant_followup_time,
204 libp2p_max_messages_per_rpc: opt.libp2p_max_messages_per_rpc,
205 libp2p_gossip_retransmission: opt.libp2p_gossip_retransmission,
206 libp2p_flood_publish: opt.libp2p_flood_publish,
207 libp2p_duplicate_cache_time: opt.libp2p_duplicate_cache_time,
208 libp2p_fanout_ttl: opt.libp2p_fanout_ttl,
209 libp2p_heartbeat_initial_delay: opt.libp2p_heartbeat_initial_delay,
210 libp2p_gossip_factor: opt.libp2p_gossip_factor,
211 libp2p_gossip_lazy: opt.libp2p_gossip_lazy,
212 libp2p_dht_put_quorum: opt.libp2p_dht_put_quorum,
213 };
214
215 let proposal_fetcher_config = opt.proposal_fetcher_config;
216
217 let persistence = storage_opt.create().await?;
218
219 let ctx = match modules.http {
223 Some(http_opt) => {
224 let mut http_opt = api::Options::from(http_opt);
226 if let Some(query) = modules.query {
227 http_opt = storage_opt.enable_query_module(http_opt, query);
228 }
229 if let Some(submit) = modules.submit {
230 http_opt = http_opt.submit(submit);
231 }
232 if let Some(status) = modules.status {
233 http_opt = http_opt.status(status);
234 }
235
236 if let Some(catchup) = modules.catchup {
237 http_opt = http_opt.catchup(catchup);
238 }
239 if let Some(hotshot_events) = modules.hotshot_events {
240 http_opt = http_opt.hotshot_events(hotshot_events);
241 }
242 if let Some(explorer) = modules.explorer {
243 http_opt = http_opt.explorer(explorer);
244 }
245 if let Some(light_client) = modules.light_client {
246 http_opt = http_opt.light_client(light_client);
247 }
248 if let Some(config) = modules.config {
249 http_opt = http_opt
250 .config(config)
251 .public_node_config(public_node_config);
252 }
253
254 http_opt
255 .serve(move |metrics, consumer, storage| {
256 async move {
257 init_node(
258 genesis,
259 network_params,
260 metrics,
261 persistence,
262 l1_params,
263 storage,
264 consumer,
265 opt.is_da,
266 opt.identity,
267 proposal_fetcher_config,
268 )
269 .await
270 }
271 .boxed()
272 })
273 .await?
274 },
275 None => {
276 init_node(
277 genesis,
278 network_params,
279 Box::new(NoMetrics),
280 persistence,
281 l1_params,
282 None,
283 NullEventConsumer,
284 opt.is_da,
285 opt.identity,
286 proposal_fetcher_config,
287 )
288 .await?
289 },
290 };
291
292 Ok(ctx)
293}
294
295#[cfg(test)]
296mod test {
297 use std::time::Duration;
298
299 use espresso_types::{
300 PubKey, Upgrade, UpgradeType,
301 v0_1::{UpgradeMode, ViewBasedUpgrade},
302 };
303 use hotshot_types::{light_client::StateKeyPair, traits::signature_key::SignatureKey, x25519};
304 use http_client::{Client, Url, error::ClientErr};
305 use tagged_base64::TaggedBase64;
306 use tempfile::TempDir;
307 use test_utils::reserve_tcp_port;
308 use tokio::spawn;
309 use vbs::version::Version;
310
311 use super::*;
312 use crate::{
313 SequencerApiVersion,
314 api::options::Http,
315 genesis::{L1Finalized, StakeTableConfig},
316 persistence::fs,
317 };
318
319 #[test_log::test(tokio::test(flavor = "multi_thread"))]
320 async fn test_startup_before_orchestrator() {
321 let (pub_key, priv_key) = PubKey::generated_from_seed_indexed([0; 32], 0);
322 let state_key = StateKeyPair::generate_from_seed_indexed([0; 32], 0);
323 let x25519_kp = x25519::Keypair::generate().unwrap();
324
325 let port1 = reserve_tcp_port().expect("OS should have ephemeral ports available");
326 let port2 = reserve_tcp_port().expect("OS should have ephemeral ports available");
327 let tmp = TempDir::new().unwrap();
328
329 let genesis_file = tmp.path().join("genesis.toml");
330 let genesis = Genesis {
331 chain_config: Default::default(),
332 stake_table: StakeTableConfig { capacity: 10 },
333 accounts: Default::default(),
334 l1_finalized: L1Finalized::Number { number: 0 },
335 header: Default::default(),
336 upgrades: [(
339 Version { major: 0, minor: 2 },
340 Upgrade {
341 mode: UpgradeMode::View(ViewBasedUpgrade {
342 start_proposing_view: 100,
343 stop_proposing_view: 200,
344 start_voting_view: None,
345 stop_voting_view: None,
346 }),
347 upgrade_type: UpgradeType::Fee {
348 chain_config: Default::default(),
349 },
350 },
351 )]
352 .into_iter()
353 .collect(),
354 base_version: Version { major: 0, minor: 1 },
355 upgrade_version: Version { major: 0, minor: 2 },
356 epoch_height: None,
357 drb_difficulty: None,
358 drb_upgrade_difficulty: None,
359 epoch_start_block: None,
360 stake_table_capacity: None,
361 genesis_version: Version { major: 0, minor: 1 },
362 da_committees: None,
363 };
364 genesis.to_file(&genesis_file).unwrap();
365
366 let modules = Modules {
367 http: Some(Http::with_port(port1)),
368 query: Some(Default::default()),
369 storage_fs: Some(fs::Options::new(tmp.path().into())),
370 config: Some(Default::default()),
371 ..Default::default()
372 };
373 let opt = Options::parse_from([
374 "sequencer",
375 "--private-staking-key",
376 &priv_key.to_tagged_base64().expect("valid key").to_string(),
377 "--private-state-key",
378 &state_key
379 .sign_key_ref()
380 .to_tagged_base64()
381 .expect("valid key")
382 .to_string(),
383 "--private-x25519-key",
384 &TaggedBase64::try_from(x25519_kp.secret_key())
385 .expect("valid key")
386 .to_string(),
387 "--cliquenet-bind-address",
388 &format!("127.0.0.1:{port2}"),
389 "--libp2p-advertise-address",
393 "127.0.0.1:0",
394 "--genesis-file",
395 &genesis_file.display().to_string(),
396 ]);
397
398 tracing::info!(port = %port1, "starting sequencer");
402 let public_node_config = PublicNodeConfig::new(&opt, &modules, &genesis);
403 let task = spawn(async move {
404 if let Err(err) = init_with_storage(
405 genesis,
406 modules,
407 opt,
408 fs::Options::new(tmp.path().into()),
409 public_node_config,
410 )
411 .await
412 {
413 tracing::error!("failed to start sequencer: {err:#}");
414 }
415 });
416
417 tracing::info!("waiting for API to start");
420 let url: Url = format!("http://localhost:{port1}").parse().unwrap();
421 let client = Client::<ClientErr, SequencerApiVersion>::new(url.clone());
422 assert!(client.connect(Some(Duration::from_secs(60))).await);
423 client.get::<()>("healthcheck").send().await.unwrap();
424
425 let res = reqwest::get(
428 url.join(&espresso_api::routes::v1::status_metrics())
429 .unwrap(),
430 )
431 .await
432 .unwrap();
433 assert!(res.status().is_success(), "{}", res.status());
434 let metrics = res.text().await.unwrap();
435 let lines = metrics.lines().collect::<Vec<_>>();
436 assert!(
437 lines.contains(&format!("consensus_node{{key=\"{pub_key}\"}} 1").as_str()),
438 "{lines:#?}"
439 );
440 assert!(
441 lines.contains(
442 &format!(
443 "consensus_version{{desc=\"{}\",rev=\"{}\",timestamp=\"{}\"}} 1",
444 espresso_utils::build_info::GIT_DESCRIBE,
445 espresso_utils::build_info::GIT_SHA,
446 espresso_utils::build_info::GIT_COMMIT_TIMESTAMP,
447 )
448 .as_str()
449 ),
450 "{lines:#?}"
451 );
452 let build_info_line = lines
453 .iter()
454 .find(|l| l.starts_with("consensus_build_info{"));
455 assert!(
456 build_info_line.is_some(),
457 "missing consensus_build_info metric: {lines:#?}"
458 );
459 let build_info_line = build_info_line.unwrap();
460 assert!(
461 build_info_line.contains("modified="),
462 "expected modified= in build_info: {lines:#?}"
463 );
464 assert!(
465 build_info_line.contains("features="),
466 "expected features= in build_info: {lines:#?}"
467 );
468 assert!(
469 build_info_line.contains("testing"),
470 "expected testing in features: {lines:#?}"
471 );
472 let genesis_line = concat!(
473 r#"consensus_genesis{base_version="0.1",genesis_version="0.1","#,
474 r#"upgrade_version="0.2"} 1"#
475 );
476 assert!(
477 lines.contains(&genesis_line),
478 "missing consensus_genesis metric: {lines:#?}"
479 );
480 assert!(
481 lines.contains(&r#"consensus_genesis_upgrade{version="0.2"} 1"#),
482 "missing consensus_genesis_upgrade metric: {lines:#?}"
483 );
484
485 let res = reqwest::Client::new()
489 .get(
490 url.join(&espresso_api::routes::v1::config_runtime())
491 .unwrap(),
492 )
493 .header(reqwest::header::ACCEPT, "application/json")
494 .send()
495 .await
496 .unwrap();
497 assert!(
498 res.status().is_success(),
499 "config/runtime status: {}",
500 res.status()
501 );
502 let node_cfg: serde_json::Value = res.json().await.expect("config/runtime returns JSON");
503 assert_eq!(
504 node_cfg["cliquenet_bind_address"],
505 serde_json::Value::String(format!("127.0.0.1:{port2}")),
506 "cliquenet_bind_address mismatch: {node_cfg}"
507 );
508 assert_eq!(
509 node_cfg["is_da"],
510 serde_json::Value::Bool(false),
511 "is_da mismatch: {node_cfg}"
512 );
513 let top_level = node_cfg
514 .as_object()
515 .expect("config/runtime returns a JSON object");
516 for key in top_level.keys() {
517 assert!(
518 !key.to_lowercase().contains("private"),
519 "top-level key '{key}' contains 'private': {node_cfg}"
520 );
521 }
522 assert_eq!(
523 node_cfg["genesis"]["base_version"],
524 serde_json::Value::String("0.1".into()),
525 "genesis.base_version mismatch: {node_cfg}"
526 );
527 assert_eq!(
528 node_cfg["genesis"]["upgrade"][0]["version"],
529 serde_json::Value::String("0.2".into()),
530 "genesis.upgrade[0].version mismatch: {node_cfg}"
531 );
532 assert_eq!(
533 node_cfg["genesis"]["upgrade"][0]["start_proposing_view"],
534 serde_json::Value::Number(100.into()),
535 "genesis.upgrade[0].start_proposing_view mismatch: {node_cfg}"
536 );
537
538 task.abort();
539 }
540}