1pub mod client;
11
12use std::{
13 collections::{HashMap, HashSet},
14 fs,
15 fs::OpenOptions,
16 io,
17 sync::Arc,
18 time::Duration,
19};
20
21use alloy::primitives::U256;
22use async_lock::RwLock;
23use axum::{
24 Router,
25 body::Bytes,
26 extract::{Path, State},
27 http::{HeaderMap, StatusCode},
28 response::Response,
29 routing::{get, post},
30};
31use client::{BenchResults, BenchResultsDownloadConfig};
32use csv::Writer;
33use futures::{StreamExt, stream::FuturesUnordered};
34use hotshot_types::{
35 PeerConfig,
36 network::{BuilderType, NetworkConfig, PublicKeysFile},
37 traits::{
38 node_implementation::NodeType,
39 signature_key::{SignatureKey, StakeTableEntryType},
40 },
41};
42use http_client::{Url, error::ClientErr};
43use http_wire::{self as wire, ServerError, WireVersion, cors_layer, healthcheck_response};
44use libp2p_identity::{
45 Keypair, PeerId,
46 ed25519::{Keypair as EdKeypair, SecretKey},
47};
48use multiaddr::Multiaddr;
49use serde::de::DeserializeOwned;
50use tokio::net::TcpListener;
51use vbs::{BinarySerializer, Serializer};
52
53pub type OrchestratorVersion = WireVersion;
58
59#[must_use]
63pub fn libp2p_generate_indexed_identity(seed: [u8; 32], index: u64) -> Keypair {
64 let mut hasher = blake3::Hasher::new();
65 hasher.update(&seed);
66 hasher.update(&index.to_le_bytes());
67 let new_seed = *hasher.finalize().as_bytes();
68 let sk_bytes = SecretKey::try_from_bytes(new_seed).unwrap();
69 <EdKeypair as From<SecretKey>>::from(sk_bytes).into()
70}
71
72#[derive(Default, Clone)]
74#[allow(clippy::struct_excessive_bools)]
75struct OrchestratorState<TYPES: NodeType> {
76 latest_index: u16,
78 tmp_latest_index: u16,
80 config: NetworkConfig<TYPES>,
82 peer_pub_ready: bool,
84 pub_posted: HashMap<Vec<u8>, (u64, bool)>,
86 start: bool,
89 nodes_connected: HashSet<PeerConfig<TYPES>>,
91 bench_results: BenchResults,
93 nodes_post_results: u64,
95 manual_start_allowed: bool,
97 accepting_new_keys: bool,
99 builders: Vec<Url>,
101 fixed_stake_table: bool,
103}
104
105impl<TYPES: NodeType> OrchestratorState<TYPES> {
106 pub fn new(network_config: NetworkConfig<TYPES>) -> Self {
108 let mut peer_pub_ready = false;
109 let mut fixed_stake_table = false;
110
111 if network_config.config.known_nodes_with_stake.is_empty() {
112 println!(
113 "No nodes were loaded from the config file. Nodes will be allowed to register \
114 dynamically."
115 );
116 } else {
117 println!("Initializing orchestrator with fixed stake table.");
118 peer_pub_ready = true;
119 fixed_stake_table = true;
120 }
121
122 let builders = if matches!(network_config.builder, BuilderType::External) {
123 network_config.config.builder_urls.clone().into()
124 } else {
125 vec![]
126 };
127
128 OrchestratorState {
129 latest_index: 0,
130 tmp_latest_index: 0,
131 config: network_config,
132 peer_pub_ready,
133 pub_posted: HashMap::new(),
134 nodes_connected: HashSet::new(),
135 start: false,
136 bench_results: BenchResults::default(),
137 nodes_post_results: 0,
138 manual_start_allowed: true,
139 accepting_new_keys: true,
140 builders,
141 fixed_stake_table,
142 }
143 }
144
145 pub fn output_to_csv(&self) {
147 let output_csv = BenchResultsDownloadConfig {
148 commit_sha: self.config.commit_sha.clone(),
149 total_nodes: self.config.config.num_nodes_with_stake.into(),
150 da_committee_size: self.config.config.da_staked_committee_size,
151 fixed_leader_for_gpuvid: self.config.config.fixed_leader_for_gpuvid,
152 transactions_per_round: self.config.transactions_per_round,
153 transaction_size: self.bench_results.transaction_size_in_bytes,
154 rounds: self.config.rounds,
155 partial_results: self.bench_results.partial_results.clone(),
156 avg_latency_in_sec: self.bench_results.avg_latency_in_sec,
157 minimum_latency_in_sec: self.bench_results.minimum_latency_in_sec,
158 maximum_latency_in_sec: self.bench_results.maximum_latency_in_sec,
159 throughput_bytes_per_sec: self.bench_results.throughput_bytes_per_sec,
160 total_transactions_committed: self.bench_results.total_transactions_committed,
161 total_time_elapsed_in_sec: self.bench_results.total_time_elapsed_in_sec,
162 total_num_views: self.bench_results.total_num_views,
163 failed_num_views: self.bench_results.failed_num_views,
164 committee_type: self.bench_results.committee_type.clone(),
165 };
166 let results_csv_file = OpenOptions::new()
168 .create(true)
169 .append(true) .open("scripts/benchmarks_results/results.csv")
171 .unwrap();
172 let mut wtr = Writer::from_writer(results_csv_file);
174 let _ = wtr.serialize(output_csv);
175 let _ = wtr.flush();
176 println!("Results successfully saved in scripts/benchmarks_results/results.csv");
177 }
178}
179
180pub trait OrchestratorApi<TYPES: NodeType> {
182 fn post_identity(
187 &mut self,
188 libp2p_address: Option<Multiaddr>,
189 libp2p_public_key: Option<PeerId>,
190 ) -> Result<u16, ServerError>;
191 fn post_getconfig(&mut self, _node_index: u16) -> Result<NetworkConfig<TYPES>, ServerError>;
195 fn get_tmp_node_index(&mut self) -> Result<u16, ServerError>;
199 fn register_public_key(
203 &mut self,
204 pubkey: &mut Vec<u8>,
205 is_da: bool,
206 libp2p_address: Option<Multiaddr>,
207 libp2p_public_key: Option<PeerId>,
208 ) -> Result<(u64, bool), ServerError>;
209 fn peer_pub_ready(&self) -> Result<bool, ServerError>;
213 fn post_config_after_peer_collected(&mut self) -> Result<NetworkConfig<TYPES>, ServerError>;
217 fn get_start(&self) -> Result<bool, ServerError>;
221 fn post_run_results(&mut self, metrics: BenchResults) -> Result<(), ServerError>;
225 fn post_ready(&mut self, peer_config: &PeerConfig<TYPES>) -> Result<(), ServerError>;
229 fn post_manual_start(&mut self, password_bytes: Vec<u8>) -> Result<(), ServerError>;
233 fn post_builder(&mut self, builder: Url) -> Result<(), ServerError>;
237 fn get_builders(&self) -> Result<Vec<Url>, ServerError>;
241}
242
243impl<TYPES: NodeType> OrchestratorState<TYPES>
244where
245 TYPES::SignatureKey: serde::Serialize + Clone + SignatureKey + 'static,
246{
247 fn register_unknown(
250 &mut self,
251 pubkey: &mut Vec<u8>,
252 da_requested: bool,
253 libp2p_address: Option<Multiaddr>,
254 libp2p_public_key: Option<PeerId>,
255 ) -> Result<(u64, bool), ServerError> {
256 if let Some((node_index, is_da)) = self.pub_posted.get(pubkey) {
257 return Ok((*node_index, *is_da));
258 }
259
260 if !self.accepting_new_keys {
261 return Err(ServerError {
262 status: StatusCode::FORBIDDEN,
263 message: "Network has been started manually, and is no longer registering new \
264 keys."
265 .to_string(),
266 });
267 }
268
269 let node_index = self.pub_posted.len() as u64;
270
271 let staked_pubkey = PeerConfig::<TYPES>::from_bytes(pubkey).unwrap();
273
274 self.config
275 .config
276 .known_nodes_with_stake
277 .push(staked_pubkey.clone());
278
279 let mut added_to_da = false;
280
281 let da_full =
282 self.config.config.known_da_nodes.len() >= self.config.config.da_staked_committee_size;
283
284 #[allow(clippy::nonminimal_bool)]
285 if (self.config.indexed_da || (!self.config.indexed_da && da_requested)) && !da_full {
293 self.config.config.known_da_nodes.push(staked_pubkey);
294 added_to_da = true;
295 }
296
297 self.pub_posted
298 .insert(pubkey.clone(), (node_index, added_to_da));
299
300 if self.config.libp2p_config.clone().is_some()
303 && let (Some(libp2p_public_key), Some(libp2p_address)) =
304 (libp2p_public_key, libp2p_address)
305 {
306 self.config
308 .libp2p_config
309 .as_mut()
310 .unwrap()
311 .bootstrap_nodes
312 .push((libp2p_public_key, libp2p_address));
313 }
314
315 tracing::error!("Posted public key for node_index {node_index}");
316
317 if node_index + 1 >= (self.config.config.num_nodes_with_stake.get() as u64) {
320 self.peer_pub_ready = true;
321 self.accepting_new_keys = false;
322 }
323 Ok((node_index, added_to_da))
324 }
325
326 fn register_from_list(
328 &mut self,
329 pubkey: &mut Vec<u8>,
330 da_requested: bool,
331 libp2p_address: Option<Multiaddr>,
332 libp2p_public_key: Option<PeerId>,
333 ) -> Result<(u64, bool), ServerError> {
334 if let Some((node_index, is_da)) = self.pub_posted.get(pubkey) {
336 return Ok((*node_index, *is_da));
337 }
338
339 let staked_pubkey = PeerConfig::<TYPES>::from_bytes(pubkey).unwrap();
341
342 let Some((node_index, node_config)) =
344 self.config.public_keys.iter().enumerate().find(|keys| {
345 keys.1.stake_table_key == staked_pubkey.stake_table_entry.public_key()
346 })
347 else {
348 return Err(ServerError {
349 status: StatusCode::FORBIDDEN,
350 message: "You are unauthorized to register with the orchestrator".to_string(),
351 });
352 };
353
354 if node_config.da != da_requested {
356 return Err(ServerError {
357 status: StatusCode::BAD_REQUEST,
358 message: format!(
359 "Mismatch in DA status in registration for node {}. DA requested: {}, \
360 expected: {}",
361 node_index, da_requested, node_config.da
362 ),
363 });
364 }
365
366 let added_to_da = node_config.da;
367
368 self.pub_posted
369 .insert(pubkey.clone(), (node_index as u64, added_to_da));
370
371 if self.config.libp2p_config.clone().is_some()
374 && let (Some(libp2p_public_key), Some(libp2p_address)) =
375 (libp2p_public_key, libp2p_address)
376 {
377 self.config
379 .libp2p_config
380 .as_mut()
381 .unwrap()
382 .bootstrap_nodes
383 .push((libp2p_public_key, libp2p_address));
384 }
385
386 tracing::error!("Node {node_index} has registered.");
387
388 Ok((node_index as u64, added_to_da))
389 }
390}
391
392impl<TYPES: NodeType> OrchestratorApi<TYPES> for OrchestratorState<TYPES>
393where
394 TYPES::SignatureKey: serde::Serialize + Clone + SignatureKey + 'static,
395{
396 fn post_identity(
401 &mut self,
402 libp2p_address: Option<Multiaddr>,
403 libp2p_public_key: Option<PeerId>,
404 ) -> Result<u16, ServerError> {
405 let node_index = self.latest_index;
406 self.latest_index += 1;
407
408 if usize::from(node_index) >= self.config.config.num_nodes_with_stake.get() {
409 return Err(ServerError {
410 status: StatusCode::BAD_REQUEST,
411 message: "Network has reached capacity".to_string(),
412 });
413 }
414
415 if self.config.libp2p_config.clone().is_some()
418 && let (Some(libp2p_public_key), Some(libp2p_address)) =
419 (libp2p_public_key, libp2p_address)
420 {
421 self.config
423 .libp2p_config
424 .as_mut()
425 .unwrap()
426 .bootstrap_nodes
427 .push((libp2p_public_key, libp2p_address));
428 }
429 Ok(node_index)
430 }
431
432 fn post_getconfig(&mut self, _node_index: u16) -> Result<NetworkConfig<TYPES>, ServerError> {
435 Ok(self.config.clone())
436 }
437
438 fn get_tmp_node_index(&mut self) -> Result<u16, ServerError> {
440 let tmp_node_index = self.tmp_latest_index;
441 self.tmp_latest_index += 1;
442
443 if usize::from(tmp_node_index) >= self.config.config.num_nodes_with_stake.get() {
444 return Err(ServerError {
445 status: StatusCode::BAD_REQUEST,
446 message: "Node index getter for key pair generation has reached capacity"
447 .to_string(),
448 });
449 }
450 Ok(tmp_node_index)
451 }
452
453 fn register_public_key(
454 &mut self,
455 pubkey: &mut Vec<u8>,
456 da_requested: bool,
457 libp2p_address: Option<Multiaddr>,
458 libp2p_public_key: Option<PeerId>,
459 ) -> Result<(u64, bool), ServerError> {
460 if self.fixed_stake_table {
461 self.register_from_list(pubkey, da_requested, libp2p_address, libp2p_public_key)
462 } else {
463 self.register_unknown(pubkey, da_requested, libp2p_address, libp2p_public_key)
464 }
465 }
466
467 fn peer_pub_ready(&self) -> Result<bool, ServerError> {
468 if !self.peer_pub_ready {
469 return Err(ServerError {
470 status: StatusCode::BAD_REQUEST,
471 message: "Peer's public configs are not ready".to_string(),
472 });
473 }
474 Ok(self.peer_pub_ready)
475 }
476
477 fn post_config_after_peer_collected(&mut self) -> Result<NetworkConfig<TYPES>, ServerError> {
478 if !self.peer_pub_ready {
479 return Err(ServerError {
480 status: StatusCode::BAD_REQUEST,
481 message: "Peer's public configs are not ready".to_string(),
482 });
483 }
484
485 Ok(self.config.clone())
486 }
487
488 fn get_start(&self) -> Result<bool, ServerError> {
489 if !self.start {
491 return Err(ServerError {
492 status: StatusCode::BAD_REQUEST,
493 message: "Network is not ready to start".to_string(),
494 });
495 }
496 Ok(self.start)
497 }
498
499 fn post_ready(&mut self, peer_config: &PeerConfig<TYPES>) -> Result<(), ServerError> {
501 if !self
504 .config
505 .config
506 .known_nodes_with_stake
507 .contains(peer_config)
508 {
509 return Err(ServerError {
510 status: StatusCode::FORBIDDEN,
511 message: "You are unauthorized to register with the orchestrator".to_string(),
512 });
513 }
514
515 if self.nodes_connected.insert(peer_config.clone()) {
517 tracing::error!(
518 "Node {peer_config} connected. Total nodes connected: {}",
519 self.nodes_connected.len()
520 );
521 }
522
523 if self.nodes_connected.len() as u64 * self.config.config.start_threshold.1
525 >= (self.config.config.num_nodes_with_stake.get() as u64)
526 * self.config.config.start_threshold.0
527 {
528 self.accepting_new_keys = false;
529 self.manual_start_allowed = false;
530 self.start = true;
531 }
532
533 Ok(())
534 }
535
536 fn post_manual_start(&mut self, password_bytes: Vec<u8>) -> Result<(), ServerError> {
538 if !self.manual_start_allowed {
539 return Err(ServerError {
540 status: StatusCode::FORBIDDEN,
541 message: "Configs have already been distributed to nodes, and the network can no \
542 longer be started manually."
543 .to_string(),
544 });
545 }
546
547 let password = String::from_utf8(password_bytes)
548 .expect("Failed to decode raw password as UTF-8 string.");
549
550 if self.config.manual_start_password != Some(password) {
552 return Err(ServerError {
553 status: StatusCode::FORBIDDEN,
554 message: "Incorrect password.".to_string(),
555 });
556 }
557
558 let registered_nodes_with_stake = self.config.config.known_nodes_with_stake.len();
559 let registered_da_nodes = self.config.config.known_da_nodes.len();
560
561 if registered_da_nodes > 1 {
562 self.config.config.num_nodes_with_stake =
563 std::num::NonZeroUsize::new(registered_nodes_with_stake)
564 .expect("Failed to convert to NonZeroUsize; this should be impossible.");
565
566 self.config.config.da_staked_committee_size = registered_da_nodes;
567 } else {
568 return Err(ServerError {
569 status: StatusCode::FORBIDDEN,
570 message: format!(
571 "We cannot manually start the network, because we only have \
572 {registered_nodes_with_stake} nodes with stake registered, with \
573 {registered_da_nodes} DA nodes."
574 ),
575 });
576 }
577
578 self.accepting_new_keys = false;
579 self.manual_start_allowed = false;
580 self.peer_pub_ready = true;
581 self.start = true;
582
583 Ok(())
584 }
585
586 fn post_run_results(&mut self, metrics: BenchResults) -> Result<(), ServerError> {
588 if metrics.total_transactions_committed != 0 {
589 if self.bench_results.total_transactions_committed == 0 {
591 self.bench_results = metrics;
592 } else {
593 let cur_metrics = self.bench_results.clone();
595 self.bench_results.avg_latency_in_sec = (metrics.avg_latency_in_sec
596 * metrics.num_latency
597 + cur_metrics.avg_latency_in_sec * cur_metrics.num_latency)
598 / (metrics.num_latency + cur_metrics.num_latency);
599 self.bench_results.num_latency += metrics.num_latency;
600 self.bench_results.minimum_latency_in_sec = metrics
601 .minimum_latency_in_sec
602 .min(cur_metrics.minimum_latency_in_sec);
603 self.bench_results.maximum_latency_in_sec = metrics
604 .maximum_latency_in_sec
605 .max(cur_metrics.maximum_latency_in_sec);
606 self.bench_results.throughput_bytes_per_sec = metrics
607 .throughput_bytes_per_sec
608 .max(cur_metrics.throughput_bytes_per_sec);
609 self.bench_results.total_transactions_committed = metrics
610 .total_transactions_committed
611 .max(cur_metrics.total_transactions_committed);
612 self.bench_results.total_time_elapsed_in_sec = metrics
613 .total_time_elapsed_in_sec
614 .max(cur_metrics.total_time_elapsed_in_sec);
615 self.bench_results.total_num_views =
616 metrics.total_num_views.min(cur_metrics.total_num_views);
617 self.bench_results.failed_num_views =
618 metrics.failed_num_views.max(cur_metrics.failed_num_views);
619 }
620 }
621 self.nodes_post_results += 1;
622 if self.bench_results.partial_results == "Unset" {
623 self.bench_results.partial_results = "One".to_string();
624 self.bench_results.printout();
625 self.output_to_csv();
626 }
627 if self.bench_results.partial_results == "One"
628 && self.nodes_post_results >= (self.config.config.da_staked_committee_size as u64 / 2)
629 {
630 self.bench_results.partial_results = "HalfDA".to_string();
631 self.bench_results.printout();
632 self.output_to_csv();
633 }
634 if self.bench_results.partial_results == "HalfDA"
635 && self.nodes_post_results >= (self.config.config.num_nodes_with_stake.get() as u64 / 2)
636 {
637 self.bench_results.partial_results = "Half".to_string();
638 self.bench_results.printout();
639 self.output_to_csv();
640 }
641 if self.bench_results.partial_results != "Full"
642 && self.nodes_post_results >= (self.config.config.num_nodes_with_stake.get() as u64)
643 {
644 self.bench_results.partial_results = "Full".to_string();
645 self.bench_results.printout();
646 self.output_to_csv();
647 }
648 Ok(())
649 }
650
651 fn post_builder(&mut self, builder: Url) -> Result<(), ServerError> {
652 self.builders.push(builder);
653 Ok(())
654 }
655
656 fn get_builders(&self) -> Result<Vec<Url>, ServerError> {
657 if !matches!(self.config.builder, BuilderType::External)
658 && self.builders.len() != self.config.config.da_staked_committee_size
659 {
660 return Err(ServerError {
661 status: StatusCode::NOT_FOUND,
662 message: "Not all builders are registered yet".to_string(),
663 });
664 }
665 Ok(self.builders.clone())
666 }
667}
668
669type SharedOrchestratorState<TYPES> = Arc<RwLock<OrchestratorState<TYPES>>>;
671
672fn malformed_body() -> ServerError {
673 ServerError {
674 status: StatusCode::BAD_REQUEST,
675 message: "Malformed body".to_string(),
676 }
677}
678
679fn decode_wrapped_body<T: DeserializeOwned>(body: &[u8]) -> Result<T, ServerError> {
686 body.get(12..)
687 .and_then(|inner| Serializer::<OrchestratorVersion>::deserialize(inner).ok())
688 .ok_or_else(malformed_body)
689}
690
691fn decode_wrapped_peer_config<TYPES: NodeType>(
694 body: &[u8],
695) -> Result<PeerConfig<TYPES>, ServerError> {
696 body.get(12..)
697 .and_then(PeerConfig::<TYPES>::from_bytes)
698 .ok_or_else(malformed_body)
699}
700
701async fn healthcheck(headers: HeaderMap) -> Response {
702 healthcheck_response(&headers)
703}
704
705async fn post_identity<TYPES: NodeType>(
706 State(state): State<SharedOrchestratorState<TYPES>>,
707 headers: HeaderMap,
708 body: Bytes,
709) -> Response {
710 let result = match decode_wrapped_body::<(Option<Multiaddr>, Option<PeerId>)>(&body) {
711 Ok((libp2p_address, libp2p_public_key)) => state
712 .write()
713 .await
714 .post_identity(libp2p_address, libp2p_public_key),
715 Err(err) => Err(err),
716 };
717 wire::respond(&headers, result)
718}
719
720async fn post_getconfig<TYPES: NodeType>(
721 State(state): State<SharedOrchestratorState<TYPES>>,
722 Path(node_index): Path<u16>,
723 headers: HeaderMap,
724) -> Response {
725 let result = state.write().await.post_getconfig(node_index);
726 wire::respond(&headers, result)
727}
728
729async fn get_tmp_node_index<TYPES: NodeType>(
730 State(state): State<SharedOrchestratorState<TYPES>>,
731 headers: HeaderMap,
732) -> Response {
733 let result = state.write().await.get_tmp_node_index();
734 wire::respond(&headers, result)
735}
736
737async fn post_pubkey<TYPES: NodeType>(
738 State(state): State<SharedOrchestratorState<TYPES>>,
739 Path(is_da): Path<bool>,
740 headers: HeaderMap,
741 body: Bytes,
742) -> Response {
743 let result = match decode_wrapped_body::<(Vec<u8>, Option<Multiaddr>, Option<PeerId>)>(&body) {
744 Ok((mut pubkey, libp2p_address, libp2p_public_key)) => state
745 .write()
746 .await
747 .register_public_key(&mut pubkey, is_da, libp2p_address, libp2p_public_key),
748 Err(err) => Err(err),
749 };
750 wire::respond(&headers, result)
751}
752
753async fn peer_pubconfig_ready<TYPES: NodeType>(
754 State(state): State<SharedOrchestratorState<TYPES>>,
755 headers: HeaderMap,
756) -> Response {
757 let result = state.read().await.peer_pub_ready();
758 wire::respond(&headers, result)
759}
760
761async fn post_config_after_peer_collected<TYPES: NodeType>(
762 State(state): State<SharedOrchestratorState<TYPES>>,
763 headers: HeaderMap,
764) -> Response {
765 let result = state.write().await.post_config_after_peer_collected();
766 wire::respond(&headers, result)
767}
768
769async fn post_ready<TYPES: NodeType>(
770 State(state): State<SharedOrchestratorState<TYPES>>,
771 headers: HeaderMap,
772 body: Bytes,
773) -> Response {
774 let result = match decode_wrapped_peer_config::<TYPES>(&body) {
775 Ok(peer_config) => state.write().await.post_ready(&peer_config),
776 Err(err) => Err(err),
777 };
778 wire::respond(&headers, result)
779}
780
781async fn post_manual_start<TYPES: NodeType>(
782 State(state): State<SharedOrchestratorState<TYPES>>,
783 headers: HeaderMap,
784 body: Bytes,
785) -> Response {
786 let result = state.write().await.post_manual_start(body.to_vec());
788 wire::respond(&headers, result)
789}
790
791async fn get_start<TYPES: NodeType>(
792 State(state): State<SharedOrchestratorState<TYPES>>,
793 headers: HeaderMap,
794) -> Response {
795 let result = state.read().await.get_start();
796 wire::respond(&headers, result)
797}
798
799async fn post_results<TYPES: NodeType>(
800 State(state): State<SharedOrchestratorState<TYPES>>,
801 headers: HeaderMap,
802 body: Bytes,
803) -> Response {
804 let metrics: BenchResults = serde_json::from_slice(&body).unwrap();
807 let result = state.write().await.post_run_results(metrics);
808 wire::respond(&headers, result)
809}
810
811async fn post_builder<TYPES: NodeType>(
812 State(state): State<SharedOrchestratorState<TYPES>>,
813 headers: HeaderMap,
814 body: Bytes,
815) -> Response {
816 let result = match decode_wrapped_body::<Vec<Url>>(&body) {
817 Ok(urls) => {
818 let mut reachable = urls
819 .into_iter()
820 .map(|url| async {
821 let client: http_client::Client<ClientErr, OrchestratorVersion> =
822 http_client::Client::builder(url.clone()).build();
823 client
824 .connect(Some(Duration::from_secs(2)))
825 .await
826 .then_some(url)
827 })
828 .collect::<FuturesUnordered<_>>()
829 .filter_map(futures::future::ready);
830 match reachable.next().await {
831 Some(url) => state.write().await.post_builder(url),
832 None => Err(ServerError {
833 status: StatusCode::BAD_REQUEST,
834 message: "No reachable addresses".to_string(),
835 }),
836 }
837 },
838 Err(err) => Err(err),
839 };
840 wire::respond(&headers, result)
841}
842
843async fn get_builders<TYPES: NodeType>(
844 State(state): State<SharedOrchestratorState<TYPES>>,
845 headers: HeaderMap,
846) -> Response {
847 let result = state.read().await.get_builders();
848 wire::respond(&headers, result)
849}
850
851fn api_router<TYPES: NodeType>() -> Router<SharedOrchestratorState<TYPES>> {
855 Router::new()
856 .route("/healthcheck", get(healthcheck))
857 .route("/identity", post(post_identity::<TYPES>))
858 .route("/config/{node_index}", post(post_getconfig::<TYPES>))
859 .route("/get_tmp_node_index", post(get_tmp_node_index::<TYPES>))
860 .route("/pubkey/{is_da}", post(post_pubkey::<TYPES>))
861 .route("/peer_pub_ready", get(peer_pubconfig_ready::<TYPES>))
862 .route(
863 "/post_config_after_peer_collected",
864 post(post_config_after_peer_collected::<TYPES>),
865 )
866 .route("/ready", post(post_ready::<TYPES>))
867 .route("/start", get(get_start::<TYPES>))
868 .route("/results", post(post_results::<TYPES>))
869 .route("/manual_start", post(post_manual_start::<TYPES>))
870 .route("/builders", get(get_builders::<TYPES>))
871 .route("/builder", post(post_builder::<TYPES>))
872}
873
874fn app<TYPES: NodeType>(state: SharedOrchestratorState<TYPES>) -> Router {
877 let api = Router::new().nest("/api", api_router::<TYPES>());
878 Router::new()
879 .route("/healthcheck", get(healthcheck))
880 .merge(api.clone())
881 .nest("/v0", api)
882 .with_state(state)
883 .layer(cors_layer())
884}
885
886pub async fn run_orchestrator<TYPES: NodeType>(
890 mut network_config: NetworkConfig<TYPES>,
891 url: Url,
892) -> io::Result<()> {
893 let env_password = std::env::var("ORCHESTRATOR_MANUAL_START_PASSWORD");
894
895 if env_password.is_ok() {
896 tracing::warn!(
897 "Took orchestrator manual start password from the environment variable: \
898 ORCHESTRATOR_MANUAL_START_PASSWORD={:?}",
899 env_password
900 );
901 network_config.manual_start_password = env_password.ok();
902 }
903
904 {
907 let env_public_keys = std::env::var("ORCHESTRATOR_PUBLIC_KEYS");
908
909 if let Ok(filepath) = env_public_keys {
910 #[allow(clippy::panic)]
911 let config_file_as_string: String = fs::read_to_string(filepath.clone())
912 .unwrap_or_else(|_| panic!("Could not read config file located at {filepath}"));
913
914 let file: PublicKeysFile<TYPES> =
915 toml::from_str::<PublicKeysFile<TYPES>>(&config_file_as_string)
916 .expect("Unable to convert config file to TOML");
917
918 network_config.public_keys = file.public_keys;
919 }
920 }
921
922 network_config.config.known_nodes_with_stake = network_config
923 .public_keys
924 .iter()
925 .map(|keys| PeerConfig {
926 stake_table_entry: keys
927 .stake_table_key
928 .stake_table_entry(U256::from(keys.stake)),
929 state_ver_key: keys.state_ver_key.clone(),
930 connect_info: keys.connect_info.clone(),
931 })
932 .collect();
933
934 network_config.config.known_da_nodes = network_config
935 .public_keys
936 .iter()
937 .filter(|keys| keys.da)
938 .map(|keys| PeerConfig {
939 stake_table_entry: keys
940 .stake_table_key
941 .stake_table_entry(U256::from(keys.stake)),
942 state_ver_key: keys.state_ver_key.clone(),
943 connect_info: keys.connect_info.clone(),
944 })
945 .collect();
946
947 let state: SharedOrchestratorState<TYPES> =
948 Arc::new(RwLock::new(OrchestratorState::new(network_config)));
949 let app = app::<TYPES>(state);
950
951 let host = url
952 .host_str()
953 .ok_or_else(|| io::Error::other(format!("orchestrator url missing host: {url}")))?;
954 let port = url
955 .port_or_known_default()
956 .ok_or_else(|| io::Error::other(format!("orchestrator url missing port: {url}")))?;
957 let listener = TcpListener::bind((host, port)).await?;
958
959 tracing::error!("listening on {url:?}");
960 axum::serve(listener, app).await
961}
962
963#[cfg(test)]
964mod tests {
965 use axum::http::{Request, header};
966 use hotshot_example_types::node_types::TestTypes;
967
968 use super::*;
969
970 fn test_app() -> Router {
971 app::<TestTypes>(Arc::new(RwLock::new(OrchestratorState::new(
972 NetworkConfig::default(),
973 ))))
974 }
975
976 #[tokio::test]
978 async fn responses_carry_cors_headers() {
979 for uri in ["/healthcheck", "/api/peer_pub_ready", "/no/such/route"] {
980 let req = Request::builder()
981 .uri(uri)
982 .header(header::ORIGIN, "https://example.com")
983 .body(axum::body::Body::empty())
984 .unwrap();
985 let resp = tower::ServiceExt::oneshot(test_app(), req).await.unwrap();
986 assert_eq!(
987 resp.headers()
988 .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
989 .unwrap_or_else(|| panic!("no CORS header on {uri}")),
990 "*",
991 "{uri}"
992 );
993 }
994 }
995
996 #[tokio::test]
998 async fn versioned_and_unversioned_api_paths_route() {
999 for uri in ["/api/peer_pub_ready", "/v0/api/peer_pub_ready"] {
1000 let req = Request::builder()
1001 .uri(uri)
1002 .body(axum::body::Body::empty())
1003 .unwrap();
1004 let resp = tower::ServiceExt::oneshot(test_app(), req).await.unwrap();
1005 assert_ne!(resp.status(), StatusCode::NOT_FOUND, "{uri} did not route");
1006 }
1007 }
1008}