Skip to main content

hotshot_orchestrator/
client.rs

1// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
2// This file is part of the HotShot repository.
3
4// You should have received a copy of the MIT License
5// along with the HotShot repository. If not, see <https://mit-license.org/>.
6
7use std::{net::SocketAddr, time::Duration};
8
9use clap::Parser;
10use futures::{Future, FutureExt};
11use hotshot_types::{
12    PeerConfig, ValidatorConfig,
13    network::{NetworkConfig, NetworkConfigSource},
14    traits::node_implementation::NodeType,
15};
16use http_client::{Client, Url, error::ClientErr};
17use libp2p_identity::PeerId;
18use multiaddr::Multiaddr;
19use tokio::time::sleep;
20use tracing::{info, instrument};
21use vbs::BinarySerializer;
22
23use crate::OrchestratorVersion;
24
25/// Holds the client connection to the orchestrator
26pub struct OrchestratorClient {
27    /// the client
28    pub client: Client<ClientErr, OrchestratorVersion>,
29}
30
31/// Struct describing a benchmark result
32#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
33pub struct BenchResults {
34    /// Whether it's partial collected results
35    pub partial_results: String,
36    /// The average latency of the transactions
37    pub avg_latency_in_sec: i64,
38    /// The number of transactions that were latency measured
39    pub num_latency: i64,
40    /// The minimum latency of the transactions
41    pub minimum_latency_in_sec: i64,
42    /// The maximum latency of the transactions
43    pub maximum_latency_in_sec: i64,
44    /// The throughput of the consensus protocol = number of transactions committed per second * transaction size in bytes
45    pub throughput_bytes_per_sec: u64,
46    /// The number of transactions committed during benchmarking
47    pub total_transactions_committed: u64,
48    /// The size of each transaction in bytes
49    pub transaction_size_in_bytes: u64,
50    /// The total time elapsed for benchmarking
51    pub total_time_elapsed_in_sec: u64,
52    /// The total number of views during benchmarking
53    pub total_num_views: usize,
54    /// The number of failed views during benchmarking
55    pub failed_num_views: usize,
56    /// The membership committee type used
57    pub committee_type: String,
58}
59
60impl BenchResults {
61    /// printout the results of one example run
62    pub fn printout(&self) {
63        println!("=====================");
64        println!("{0} Benchmark results:", self.partial_results);
65        println!("Committee type: {}", self.committee_type);
66        println!(
67            "Average latency: {} seconds, Minimum latency: {} seconds, Maximum latency: {} seconds",
68            self.avg_latency_in_sec, self.minimum_latency_in_sec, self.maximum_latency_in_sec
69        );
70        println!("Throughput: {} bytes/sec", self.throughput_bytes_per_sec);
71        println!(
72            "Total transactions committed: {}",
73            self.total_transactions_committed
74        );
75        println!(
76            "Total number of views: {}, Failed number of views: {}",
77            self.total_num_views, self.failed_num_views
78        );
79        println!("=====================");
80    }
81}
82
83/// Struct describing a benchmark result needed for download, also include the config
84#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
85pub struct BenchResultsDownloadConfig {
86    // Config starting here
87    /// The commit this benchmark was run on
88    pub commit_sha: String,
89    /// Total number of nodes
90    pub total_nodes: usize,
91    /// The size of the da committee
92    pub da_committee_size: usize,
93    /// The number of fixed_leader_for_gpuvid when we enable the feature [fixed-leader-election]
94    pub fixed_leader_for_gpuvid: usize,
95    /// Number of transactions submitted per round
96    pub transactions_per_round: usize,
97    /// The size of each transaction in bytes
98    pub transaction_size: u64,
99    /// The number of rounds
100    pub rounds: usize,
101
102    // Results starting here
103    /// Whether the results are partially collected
104    /// "One" when the results are collected for one node
105    /// "Half" when the results are collective for half running nodes if not all nodes terminate successfully
106    /// "Full" if the results are successfully collected from all nodes
107    pub partial_results: String,
108    /// The average latency of the transactions
109    pub avg_latency_in_sec: i64,
110    /// The minimum latency of the transactions
111    pub minimum_latency_in_sec: i64,
112    /// The maximum latency of the transactions
113    pub maximum_latency_in_sec: i64,
114    /// The throughput of the consensus protocol = number of transactions committed per second * transaction size in bytes
115    pub throughput_bytes_per_sec: u64,
116    /// The number of transactions committed during benchmarking
117    pub total_transactions_committed: u64,
118    /// The total time elapsed for benchmarking
119    pub total_time_elapsed_in_sec: u64,
120    /// The total number of views during benchmarking
121    pub total_num_views: usize,
122    /// The number of failed views during benchmarking
123    pub failed_num_views: usize,
124    /// The membership committee type used
125    pub committee_type: String,
126}
127
128// VALIDATOR
129
130#[derive(Parser, Debug, Clone)]
131#[command(
132    name = "Multi-machine consensus",
133    about = "Simulates consensus among multiple machines"
134)]
135/// Arguments passed to the validator
136pub struct ValidatorArgs {
137    /// The address the orchestrator runs on
138    pub url: Url,
139    /// The optional advertise address to use for Libp2p
140    pub advertise_address: Option<String>,
141    /// Optional address to run builder on. Address must be accessible by other nodes
142    pub builder_address: Option<SocketAddr>,
143    /// An optional network config file to save to/load from
144    /// Allows for rejoining the network on a complete state loss
145    #[arg(short, long)]
146    pub network_config_file: Option<String>,
147}
148
149/// arguments to run multiple validators
150#[derive(Parser, Debug, Clone)]
151pub struct MultiValidatorArgs {
152    /// Number of validators to run
153    pub num_nodes: u16,
154    /// The address the orchestrator runs on
155    pub url: Url,
156    /// The optional advertise address to use for Libp2p
157    pub advertise_address: Option<String>,
158    /// An optional network config file to save to/load from
159    /// Allows for rejoining the network on a complete state loss
160    #[arg(short, long)]
161    pub network_config_file: Option<String>,
162}
163
164/// Asynchronously retrieves a `NetworkConfig` from an orchestrator.
165/// The retrieved one includes correct `node_index` and peer's public config.
166///
167/// # Errors
168/// If we are unable to get the configuration from the orchestrator
169pub async fn get_complete_config<TYPES: NodeType>(
170    client: &OrchestratorClient,
171    mut validator_config: ValidatorConfig<TYPES>,
172    libp2p_advertise_address: Option<Multiaddr>,
173    libp2p_public_key: Option<PeerId>,
174) -> anyhow::Result<(
175    NetworkConfig<TYPES>,
176    ValidatorConfig<TYPES>,
177    NetworkConfigSource,
178)> {
179    // get the configuration from the orchestrator
180    let run_config: NetworkConfig<TYPES> = client
181        .post_and_wait_all_public_keys::<TYPES>(
182            &mut validator_config,
183            libp2p_advertise_address,
184            libp2p_public_key,
185        )
186        .await;
187
188    info!(
189        "Retrieved config; our node index is {}. DA committee member: {}",
190        run_config.node_index, validator_config.is_da
191    );
192    Ok((
193        run_config,
194        validator_config,
195        NetworkConfigSource::Orchestrator,
196    ))
197}
198
199impl ValidatorArgs {
200    /// Constructs `ValidatorArgs` from `MultiValidatorArgs` and a node index.
201    ///
202    /// If `network_config_file` is present in `MultiValidatorArgs`, it appends the node index to it to create a unique file name for each node.
203    ///
204    /// # Arguments
205    ///
206    /// * `multi_args` - A `MultiValidatorArgs` instance containing the base arguments for the construction.
207    /// * `node_index` - A `u16` representing the index of the node for which the args are being constructed.
208    ///
209    /// # Returns
210    ///
211    /// This function returns a new instance of `ValidatorArgs`.
212    ///
213    /// # Examples
214    ///
215    /// ```ignore
216    /// // NOTE this is a toy example,
217    /// // the user will need to construct a multivalidatorargs since `new` does not exist
218    /// # use hotshot_orchestrator::client::MultiValidatorArgs;
219    /// let multi_args = MultiValidatorArgs::new();
220    /// let node_index = 1;
221    /// let instance = Self::from_multi_args(multi_args, node_index);
222    /// ```
223    #[must_use]
224    pub fn from_multi_args(multi_args: MultiValidatorArgs, node_index: u16) -> Self {
225        Self {
226            url: multi_args.url,
227            advertise_address: multi_args.advertise_address,
228            builder_address: None,
229            network_config_file: multi_args
230                .network_config_file
231                .map(|s| format!("{s}-{node_index}")),
232        }
233    }
234}
235
236impl OrchestratorClient {
237    /// Creates the client that will connect to the orchestrator
238    #[must_use]
239    pub fn new(url: Url) -> Self {
240        let client = Client::<ClientErr, OrchestratorVersion>::new(url);
241        // TODO ED: Add healthcheck wait here
242        OrchestratorClient { client }
243    }
244
245    /// Get the config from the orchestrator.
246    /// If the identity is provided, register the identity with the orchestrator.
247    /// If not, just retrieving the config (for passive observers)
248    ///
249    /// # Panics
250    /// if unable to convert the node index from usize into u64
251    /// (only applicable on 32 bit systems)
252    ///
253    /// # Errors
254    /// If we were unable to serialize the Libp2p data
255    #[allow(clippy::type_complexity)]
256    pub async fn get_config_without_peer<TYPES: NodeType>(
257        &self,
258        libp2p_advertise_address: Option<Multiaddr>,
259        libp2p_public_key: Option<PeerId>,
260    ) -> anyhow::Result<NetworkConfig<TYPES>> {
261        // Serialize our (possible) libp2p-specific data
262        let request_body = vbs::Serializer::<OrchestratorVersion>::serialize(&(
263            libp2p_advertise_address,
264            libp2p_public_key,
265        ))?;
266
267        let identity = |client: Client<ClientErr, OrchestratorVersion>| {
268            // We need to clone here to move it into the closure
269            let request_body = request_body.clone();
270            async move {
271                let node_index: Result<u16, ClientErr> = client
272                    .post("api/identity")
273                    .body_binary(&request_body)
274                    .expect("failed to set request body")
275                    .send()
276                    .await;
277
278                node_index
279            }
280            .boxed()
281        };
282        let node_index = self.wait_for_fn_from_orchestrator(identity).await;
283
284        // get the corresponding config
285        let f = |client: Client<ClientErr, OrchestratorVersion>| {
286            async move {
287                let config: Result<NetworkConfig<TYPES>, ClientErr> = client
288                    .post(&format!("api/config/{node_index}"))
289                    .send()
290                    .await;
291                config
292            }
293            .boxed()
294        };
295
296        let mut config = self.wait_for_fn_from_orchestrator(f).await;
297        config.node_index = From::<u16>::from(node_index);
298
299        Ok(config)
300    }
301
302    /// Post to the orchestrator and get the latest `node_index`
303    /// Then return it for the init validator config
304    /// # Panics
305    /// if unable to post
306    #[instrument(skip_all, name = "orchestrator node index for validator config")]
307    pub async fn get_node_index_for_init_validator_config(&self) -> u16 {
308        let cur_node_index = |client: Client<ClientErr, OrchestratorVersion>| {
309            async move {
310                let cur_node_index: Result<u16, ClientErr> = client
311                    .post("api/get_tmp_node_index")
312                    .send()
313                    .await
314                    .inspect_err(|err| tracing::error!("{err}"));
315
316                cur_node_index
317            }
318            .boxed()
319        };
320        self.wait_for_fn_from_orchestrator(cur_node_index).await
321    }
322
323    /// Requests the configuration from the orchestrator with the stipulation that
324    /// a successful call requires all nodes to be registered.
325    ///
326    /// Does not fail, retries internally until success.
327    #[instrument(skip_all, name = "orchestrator config")]
328    pub async fn get_config_after_collection<TYPES: NodeType>(&self) -> NetworkConfig<TYPES> {
329        // Define the request for post-register configurations
330        let get_config_after_collection = |client: Client<ClientErr, OrchestratorVersion>| {
331            async move {
332                let result = client
333                    .post("api/post_config_after_peer_collected")
334                    .send()
335                    .await;
336
337                if let Err(ref err) = result {
338                    tracing::error!("{err}");
339                }
340
341                result
342            }
343            .boxed()
344        };
345
346        // Loop until successful
347        self.wait_for_fn_from_orchestrator(get_config_after_collection)
348            .await
349    }
350
351    /// Registers a builder URL with the orchestrator
352    ///
353    /// # Panics
354    /// if unable to serialize `address`
355    pub async fn post_builder_addresses(&self, addresses: Vec<Url>) {
356        let send_builder_f = |client: Client<ClientErr, OrchestratorVersion>| {
357            let request_body = vbs::Serializer::<OrchestratorVersion>::serialize(&addresses)
358                .expect("Failed to serialize request");
359
360            async move {
361                let result: Result<_, ClientErr> = client
362                    .post("api/builder")
363                    .body_binary(&request_body)
364                    .unwrap()
365                    .send()
366                    .await
367                    .inspect_err(|err| tracing::error!("{err}"));
368                result
369            }
370            .boxed()
371        };
372        self.wait_for_fn_from_orchestrator::<_, _, ()>(send_builder_f)
373            .await;
374    }
375
376    /// Requests a builder URL from orchestrator
377    pub async fn get_builder_addresses(&self) -> Vec<Url> {
378        // Define the request for post-register configurations
379        let get_builder = |client: Client<ClientErr, OrchestratorVersion>| {
380            async move {
381                let result = client.get("api/builders").send().await;
382
383                if let Err(ref err) = result {
384                    tracing::error!("{err}");
385                }
386
387                result
388            }
389            .boxed()
390        };
391
392        // Loop until successful
393        self.wait_for_fn_from_orchestrator(get_builder).await
394    }
395
396    /// Sends my public key to the orchestrator so that it can collect all public keys
397    /// And get the updated config
398    /// Blocks until the orchestrator collects all peer's public keys/configs
399    /// # Panics
400    /// if unable to post
401    #[instrument(skip(self), name = "orchestrator public keys")]
402    pub async fn post_and_wait_all_public_keys<TYPES: NodeType>(
403        &self,
404        validator_config: &mut ValidatorConfig<TYPES>,
405        libp2p_advertise_address: Option<Multiaddr>,
406        libp2p_public_key: Option<PeerId>,
407    ) -> NetworkConfig<TYPES> {
408        let pubkey: Vec<u8> =
409            PeerConfig::<TYPES>::to_bytes(&validator_config.public_config()).clone();
410        let da_requested: bool = validator_config.is_da;
411
412        // Serialize our (possible) libp2p-specific data
413        let request_body = vbs::Serializer::<OrchestratorVersion>::serialize(&(
414            pubkey,
415            libp2p_advertise_address,
416            libp2p_public_key,
417        ))
418        .expect("failed to serialize request");
419
420        // register our public key with the orchestrator
421        let (node_index, is_da): (u64, bool) = loop {
422            let result = self
423                .client
424                .post(&format!("api/pubkey/{da_requested}"))
425                .body_binary(&request_body)
426                .expect("Failed to form request")
427                .send()
428                .await
429                .inspect_err(|err| tracing::error!("{err}"));
430
431            if let Ok((index, is_da)) = result {
432                break (index, is_da);
433            }
434
435            sleep(Duration::from_millis(250)).await;
436        };
437
438        validator_config.is_da = is_da;
439
440        // wait for all nodes' public keys
441        let wait_for_all_nodes_pub_key = |client: Client<ClientErr, OrchestratorVersion>| {
442            async move {
443                client
444                    .get("api/peer_pub_ready")
445                    .send()
446                    .await
447                    .inspect_err(|err| tracing::error!("{err}"))
448            }
449            .boxed()
450        };
451        self.wait_for_fn_from_orchestrator::<_, _, ()>(wait_for_all_nodes_pub_key)
452            .await;
453
454        let mut network_config = self.get_config_after_collection().await;
455
456        network_config.node_index = node_index;
457
458        network_config
459    }
460
461    /// Tells the orchestrator this validator is ready to start
462    /// Blocks until the orchestrator indicates all nodes are ready to start
463    /// # Panics
464    /// Panics if unable to post.
465    #[instrument(skip(self), name = "orchestrator ready signal")]
466    pub async fn wait_for_all_nodes_ready(&self, peer_config: Vec<u8>) -> bool {
467        let send_ready_f = |client: Client<ClientErr, OrchestratorVersion>| {
468            let pk = peer_config.clone();
469            async move {
470                let result: Result<_, ClientErr> = client
471                    .post("api/ready")
472                    .body_binary(&pk)
473                    .unwrap()
474                    .send()
475                    .await
476                    .inspect_err(|err| tracing::error!("{err}"));
477                result
478            }
479            .boxed()
480        };
481        self.wait_for_fn_from_orchestrator::<_, _, ()>(send_ready_f)
482            .await;
483
484        let wait_for_all_nodes_ready_f = |client: Client<ClientErr, OrchestratorVersion>| {
485            async move { client.get("api/start").send().await }.boxed()
486        };
487        self.wait_for_fn_from_orchestrator(wait_for_all_nodes_ready_f)
488            .await
489    }
490
491    /// Sends the benchmark metrics to the orchestrator
492    /// # Panics
493    /// Panics if unable to post
494    #[instrument(skip_all, name = "orchestrator metrics")]
495    pub async fn post_bench_results(&self, bench_results: BenchResults) {
496        let _send_metrics_f: Result<(), ClientErr> = self
497            .client
498            .post("api/results")
499            .body_json(&bench_results)
500            .unwrap()
501            .send()
502            .await
503            .inspect_err(|err| tracing::warn!("{err}"));
504    }
505
506    /// Generic function that waits for the orchestrator to return a non-error
507    /// Returns whatever type the given function returns
508    #[instrument(skip_all, name = "waiting for orchestrator")]
509    async fn wait_for_fn_from_orchestrator<F, Fut, GEN>(&self, f: F) -> GEN
510    where
511        F: Fn(Client<ClientErr, OrchestratorVersion>) -> Fut,
512        Fut: Future<Output = Result<GEN, ClientErr>>,
513    {
514        loop {
515            let client = self.client.clone();
516            let res = f(client).await;
517            match res {
518                Ok(x) => break x,
519                Err(err) => {
520                    tracing::info!("{err}");
521                    sleep(Duration::from_millis(250)).await;
522                },
523            }
524        }
525    }
526}