hotshot_types/
network.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::{fs, ops::Range, path::Path, time::Duration, vec};
8
9use alloy::primitives::U256;
10use clap::ValueEnum;
11use libp2p_identity::PeerId;
12use multiaddr::Multiaddr;
13use serde_inline_default::serde_inline_default;
14use thiserror::Error;
15use tracing::error;
16
17use crate::{
18    constants::{
19        ORCHESTRATOR_DEFAULT_NUM_ROUNDS, ORCHESTRATOR_DEFAULT_TRANSACTIONS_PER_ROUND,
20        ORCHESTRATOR_DEFAULT_TRANSACTION_SIZE, REQUEST_DATA_DELAY,
21    },
22    hotshot_config_file::HotShotConfigFile,
23    HotShotConfig, NodeType, ValidatorConfig,
24};
25
26/// Configuration describing a libp2p node
27#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
28pub struct Libp2pConfig {
29    /// The bootstrap nodes to connect to (multiaddress, serialized public key)
30    pub bootstrap_nodes: Vec<(PeerId, Multiaddr)>,
31}
32
33/// configuration for combined network
34#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
35pub struct CombinedNetworkConfig {
36    /// delay duration before sending a message through the secondary network
37    pub delay_duration: Duration,
38}
39
40/// a network configuration error
41#[derive(Error, Debug)]
42pub enum NetworkConfigError {
43    /// Failed to read NetworkConfig from file
44    #[error("Failed to read NetworkConfig from file")]
45    ReadFromFileError(std::io::Error),
46    /// Failed to deserialize loaded NetworkConfig
47    #[error("Failed to deserialize loaded NetworkConfig")]
48    DeserializeError(serde_json::Error),
49    /// Failed to write NetworkConfig to file
50    #[error("Failed to write NetworkConfig to file")]
51    WriteToFileError(std::io::Error),
52    /// Failed to serialize NetworkConfig
53    #[error("Failed to serialize NetworkConfig")]
54    SerializeError(serde_json::Error),
55    /// Failed to recursively create path to NetworkConfig
56    #[error("Failed to recursively create path to NetworkConfig")]
57    FailedToCreatePath(std::io::Error),
58}
59
60#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Default, ValueEnum)]
61/// configuration for builder type to use
62pub enum BuilderType {
63    /// Use external builder, [config.builder_url] must be
64    /// set to correct builder address
65    External,
66    #[default]
67    /// Simple integrated builder will be started and used by each hotshot node
68    Simple,
69    /// Random integrated builder will be started and used by each hotshot node
70    Random,
71}
72
73/// Node PeerConfig keys
74#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
75#[serde(bound(deserialize = ""))]
76pub struct PeerConfigKeys<TYPES: NodeType> {
77    /// The peer's public key
78    pub stake_table_key: TYPES::SignatureKey,
79    /// the peer's state public key
80    pub state_ver_key: TYPES::StateSignatureKey,
81    /// the peer's stake
82    pub stake: u64,
83    /// whether the node is a DA node
84    pub da: bool,
85}
86
87/// Options controlling how the random builder generates blocks
88#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
89pub struct RandomBuilderConfig {
90    /// How many transactions to include in a block
91    pub txn_in_block: u64,
92    /// How many blocks to generate per second
93    pub blocks_per_second: u32,
94    /// Range of how big a transaction can be (in bytes)
95    pub txn_size: Range<u32>,
96}
97
98impl Default for RandomBuilderConfig {
99    fn default() -> Self {
100        Self {
101            txn_in_block: 100,
102            blocks_per_second: 1,
103            txn_size: 20..100,
104        }
105    }
106}
107
108/// a network configuration
109#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
110#[serde(bound(deserialize = ""))]
111pub struct NetworkConfig<TYPES: NodeType> {
112    /// number of views to run
113    pub rounds: usize,
114    /// whether DA membership is determined by index.
115    /// if true, the first k nodes to register form the DA committee
116    /// if false, DA membership is requested by the nodes
117    pub indexed_da: bool,
118    /// number of transactions per view
119    pub transactions_per_round: usize,
120    /// password to have the orchestrator start the network,
121    /// regardless of the number of nodes connected.
122    pub manual_start_password: Option<String>,
123    /// number of bootstrap nodes
124    pub num_bootrap: usize,
125    /// timeout before starting the next view
126    pub next_view_timeout: u64,
127    /// timeout before starting next view sync round
128    pub view_sync_timeout: Duration,
129    /// The maximum amount of time a leader can wait to get a block from a builder
130    pub builder_timeout: Duration,
131    /// time to wait until we request data associated with a proposal
132    pub data_request_delay: Duration,
133    /// global index of node (for testing purposes a uid)
134    pub node_index: u64,
135    /// unique seed (for randomness? TODO)
136    pub seed: [u8; 32],
137    /// size of transactions
138    pub transaction_size: usize,
139    /// name of the key type (for debugging)
140    pub key_type_name: String,
141    /// the libp2p config
142    pub libp2p_config: Option<Libp2pConfig>,
143    /// the hotshot config
144    pub config: HotShotConfig<TYPES>,
145    /// The address for the Push CDN's "marshal", A.K.A. load balancer
146    pub cdn_marshal_address: Option<String>,
147    /// combined network config
148    pub combined_network_config: Option<CombinedNetworkConfig>,
149    /// the commit this run is based on
150    pub commit_sha: String,
151    /// builder to use
152    pub builder: BuilderType,
153    /// random builder config
154    pub random_builder: Option<RandomBuilderConfig>,
155    /// The list of public keys that are allowed to connect to the orchestrator
156    pub public_keys: Vec<PeerConfigKeys<TYPES>>,
157}
158
159/// the source of the network config
160pub enum NetworkConfigSource {
161    /// we source the network configuration from the orchestrator
162    Orchestrator,
163    /// we source the network configuration from a config file on disk
164    File,
165}
166
167impl<TYPES: NodeType> NetworkConfig<TYPES> {
168    /// Get a temporary node index for generating a validator config
169    #[must_use]
170    pub fn generate_init_validator_config(
171        cur_node_index: u16,
172        is_da: bool,
173    ) -> ValidatorConfig<TYPES> {
174        // This cur_node_index is only used for key pair generation, it's not bound with the node,
175        // later the node with the generated key pair will get a new node_index from orchestrator.
176        ValidatorConfig::generated_from_seed_indexed(
177            [0u8; 32],
178            cur_node_index.into(),
179            U256::from(1),
180            is_da,
181        )
182    }
183
184    /// Loads a `NetworkConfig` from a file.
185    ///
186    /// This function takes a file path as a string, reads the file, and then deserializes the contents into a `NetworkConfig`.
187    ///
188    /// # Arguments
189    ///
190    /// * `file` - A string representing the path to the file from which to load the `NetworkConfig`.
191    ///
192    /// # Returns
193    ///
194    /// This function returns a `Result` that contains a `NetworkConfig` if the file was successfully read and deserialized, or a `NetworkConfigError` if an error occurred.
195    ///
196    /// # Errors
197    ///
198    /// This function will return an error if the file cannot be read or if the contents cannot be deserialized into a `NetworkConfig`.
199    ///
200    /// # Examples
201    ///
202    /// ```ignore
203    /// # use hotshot_orchestrator::config::NetworkConfig;
204    /// // # use hotshot::traits::election::static_committee::StaticElectionConfig;
205    /// let file = "/path/to/my/config".to_string();
206    /// // NOTE: broken due to staticelectionconfig not being importable
207    /// // cannot import staticelectionconfig from hotshot without creating circular dependency
208    /// // making this work probably involves the `types` crate implementing a dummy
209    /// // electionconfigtype just to make this example work
210    /// let config = NetworkConfig::<TYPES, StaticElectionConfig>::from_file(file).unwrap();
211    /// ```
212    pub fn from_file(file: String) -> Result<Self, NetworkConfigError> {
213        // read from file
214        let data = match fs::read(file) {
215            Ok(data) => data,
216            Err(e) => {
217                return Err(NetworkConfigError::ReadFromFileError(e));
218            },
219        };
220
221        // deserialize
222        match serde_json::from_slice(&data) {
223            Ok(data) => Ok(data),
224            Err(e) => Err(NetworkConfigError::DeserializeError(e)),
225        }
226    }
227
228    /// Serializes the `NetworkConfig` and writes it to a file.
229    ///
230    /// This function takes a file path as a string, serializes the `NetworkConfig` into JSON format using `serde_json` and then writes the serialized data to the file.
231    ///
232    /// # Arguments
233    ///
234    /// * `file` - A string representing the path to the file where the `NetworkConfig` should be saved.
235    ///
236    /// # Returns
237    ///
238    /// This function returns a `Result` that contains `()` if the `NetworkConfig` was successfully serialized and written to the file, or a `NetworkConfigError` if an error occurred.
239    ///
240    /// # Errors
241    ///
242    /// This function will return an error if the `NetworkConfig` cannot be serialized or if the file cannot be written.
243    ///
244    /// # Examples
245    ///
246    /// ```ignore
247    /// # use hotshot_orchestrator::config::NetworkConfig;
248    /// let file = "/path/to/my/config".to_string();
249    /// let config = NetworkConfig::from_file(file);
250    /// config.to_file(file).unwrap();
251    /// ```
252    pub fn to_file(&self, file: String) -> Result<(), NetworkConfigError> {
253        // ensure the directory containing the config file exists
254        if let Some(dir) = Path::new(&file).parent() {
255            if let Err(e) = fs::create_dir_all(dir) {
256                return Err(NetworkConfigError::FailedToCreatePath(e));
257            }
258        }
259
260        // serialize
261        let serialized = match serde_json::to_string_pretty(self) {
262            Ok(data) => data,
263            Err(e) => {
264                return Err(NetworkConfigError::SerializeError(e));
265            },
266        };
267
268        // write to file
269        match fs::write(file, serialized) {
270            Ok(()) => Ok(()),
271            Err(e) => Err(NetworkConfigError::WriteToFileError(e)),
272        }
273    }
274}
275
276impl<TYPES: NodeType> Default for NetworkConfig<TYPES> {
277    fn default() -> Self {
278        Self {
279            rounds: ORCHESTRATOR_DEFAULT_NUM_ROUNDS,
280            indexed_da: true,
281            transactions_per_round: ORCHESTRATOR_DEFAULT_TRANSACTIONS_PER_ROUND,
282            node_index: 0,
283            seed: [0u8; 32],
284            transaction_size: ORCHESTRATOR_DEFAULT_TRANSACTION_SIZE,
285            manual_start_password: None,
286            libp2p_config: None,
287            config: HotShotConfigFile::hotshot_config_5_nodes_10_da().into(),
288            key_type_name: std::any::type_name::<TYPES>().to_string(),
289            cdn_marshal_address: None,
290            combined_network_config: None,
291            next_view_timeout: 10,
292            view_sync_timeout: Duration::from_secs(2),
293            num_bootrap: 5,
294            builder_timeout: Duration::from_secs(10),
295            data_request_delay: Duration::from_millis(2500),
296            commit_sha: String::new(),
297            builder: BuilderType::default(),
298            random_builder: None,
299            public_keys: vec![],
300        }
301    }
302}
303
304/// a network config stored in a file
305#[serde_inline_default]
306#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
307#[serde(bound(deserialize = ""))]
308pub struct PublicKeysFile<TYPES: NodeType> {
309    /// The list of public keys that are allowed to connect to the orchestrator
310    ///
311    /// If nonempty, this list becomes the stake table and is used to determine DA membership (ignoring the node's request).
312    #[serde(default)]
313    pub public_keys: Vec<PeerConfigKeys<TYPES>>,
314}
315
316/// a network config stored in a file
317#[serde_inline_default]
318#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
319#[serde(bound(deserialize = ""))]
320pub struct NetworkConfigFile<TYPES: NodeType> {
321    /// number of views to run
322    #[serde_inline_default(ORCHESTRATOR_DEFAULT_NUM_ROUNDS)]
323    pub rounds: usize,
324    /// number of views to run
325    #[serde(default)]
326    pub indexed_da: bool,
327    /// number of transactions per view
328    #[serde_inline_default(ORCHESTRATOR_DEFAULT_TRANSACTIONS_PER_ROUND)]
329    pub transactions_per_round: usize,
330    /// password to have the orchestrator start the network,
331    /// regardless of the number of nodes connected.
332    #[serde(default)]
333    pub manual_start_password: Option<String>,
334    /// global index of node (for testing purposes a uid)
335    #[serde(default)]
336    pub node_index: u64,
337    /// unique seed (for randomness? TODO)
338    #[serde(default)]
339    pub seed: [u8; 32],
340    /// size of transactions
341    #[serde_inline_default(ORCHESTRATOR_DEFAULT_TRANSACTION_SIZE)]
342    pub transaction_size: usize,
343    /// the hotshot config file
344    #[serde(default = "HotShotConfigFile::hotshot_config_5_nodes_10_da")]
345    pub config: HotShotConfigFile<TYPES>,
346    /// The address of the Push CDN's "marshal", A.K.A. load balancer
347    #[serde(default)]
348    pub cdn_marshal_address: Option<String>,
349    /// combined network config
350    #[serde(default)]
351    pub combined_network_config: Option<CombinedNetworkConfig>,
352    /// builder to use
353    #[serde(default)]
354    pub builder: BuilderType,
355    /// random builder configuration
356    #[serde(default)]
357    pub random_builder: Option<RandomBuilderConfig>,
358    /// The list of public keys that are allowed to connect to the orchestrator
359    ///
360    /// If nonempty, this list becomes the stake table and is used to determine DA membership (ignoring the node's request).
361    #[serde(default)]
362    pub public_keys: Vec<PeerConfigKeys<TYPES>>,
363}
364
365impl<TYPES: NodeType> From<NetworkConfigFile<TYPES>> for NetworkConfig<TYPES> {
366    fn from(val: NetworkConfigFile<TYPES>) -> Self {
367        NetworkConfig {
368            rounds: val.rounds,
369            indexed_da: val.indexed_da,
370            transactions_per_round: val.transactions_per_round,
371            node_index: 0,
372            num_bootrap: val.config.num_bootstrap,
373            manual_start_password: val.manual_start_password,
374            next_view_timeout: val.config.next_view_timeout,
375            view_sync_timeout: val.config.view_sync_timeout,
376            builder_timeout: val.config.builder_timeout,
377            data_request_delay: val
378                .config
379                .data_request_delay
380                .unwrap_or(Duration::from_millis(REQUEST_DATA_DELAY)),
381            seed: val.seed,
382            transaction_size: val.transaction_size,
383            libp2p_config: Some(Libp2pConfig {
384                bootstrap_nodes: Vec::new(),
385            }),
386            config: val.config.into(),
387            key_type_name: std::any::type_name::<TYPES>().to_string(),
388            cdn_marshal_address: val.cdn_marshal_address,
389            combined_network_config: val.combined_network_config,
390            commit_sha: String::new(),
391            builder: val.builder,
392            random_builder: val.random_builder,
393            public_keys: val.public_keys,
394        }
395    }
396}