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