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