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;
15
16use crate::{
17    HotShotConfig, NodeType, PeerConnectInfo, ValidatorConfig,
18    constants::{
19        ORCHESTRATOR_DEFAULT_NUM_ROUNDS, ORCHESTRATOR_DEFAULT_TRANSACTION_SIZE,
20        ORCHESTRATOR_DEFAULT_TRANSACTIONS_PER_ROUND, REQUEST_DATA_DELAY,
21    },
22    hotshot_config_file::HotShotConfigFile,
23};
24
25/// Configuration describing a libp2p node
26#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
27pub struct Libp2pConfig {
28    /// The bootstrap nodes to connect to (multiaddress, serialized public key)
29    pub bootstrap_nodes: Vec<(PeerId, Multiaddr)>,
30}
31
32/// configuration for combined network
33#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
34pub struct CombinedNetworkConfig {
35    /// delay duration before sending a message through the secondary network
36    pub delay_duration: Duration,
37}
38
39/// a network configuration error
40#[derive(Error, Debug)]
41pub enum NetworkConfigError {
42    /// Failed to read NetworkConfig from file
43    #[error("Failed to read NetworkConfig from file")]
44    ReadFromFileError(std::io::Error),
45    /// Failed to deserialize loaded NetworkConfig
46    #[error("Failed to deserialize loaded NetworkConfig")]
47    DeserializeError(serde_json::Error),
48    /// Failed to write NetworkConfig to file
49    #[error("Failed to write NetworkConfig to file")]
50    WriteToFileError(std::io::Error),
51    /// Failed to serialize NetworkConfig
52    #[error("Failed to serialize NetworkConfig")]
53    SerializeError(serde_json::Error),
54    /// Failed to recursively create path to NetworkConfig
55    #[error("Failed to recursively create path to NetworkConfig")]
56    FailedToCreatePath(std::io::Error),
57}
58
59#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Default, ValueEnum)]
60/// configuration for builder type to use
61pub enum BuilderType {
62    /// Use external builder, [config.builder_url] must be
63    /// set to correct builder address
64    External,
65    #[default]
66    /// Simple integrated builder will be started and used by each hotshot node
67    Simple,
68    /// Random integrated builder will be started and used by each hotshot node
69    Random,
70}
71
72/// Node PeerConfig keys
73#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
74#[serde(bound(deserialize = ""))]
75pub struct PeerConfigKeys<TYPES: NodeType> {
76    /// The peer's public key
77    pub stake_table_key: TYPES::SignatureKey,
78    /// the peer's state public key
79    pub state_ver_key: TYPES::StateSignatureKey,
80    /// the peer's stake
81    pub stake: u64,
82    /// whether the node is a DA node
83    pub da: bool,
84    pub connect_info: Option<PeerConnectInfo>,
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    pub fn generate_init_test_validator_config(
170        cur_node_index: u16,
171        is_da: bool,
172    ) -> ValidatorConfig<TYPES> {
173        // This cur_node_index is only used for key pair generation, it's not bound with the node,
174        // later the node with the generated key pair will get a new node_index from orchestrator.
175        ValidatorConfig::generated_from_seed_indexed(
176            [0u8; 32],
177            cur_node_index.into(),
178            U256::from(1),
179            is_da,
180        )
181    }
182
183    /// Loads a `NetworkConfig` from a file.
184    ///
185    /// This function takes a file path as a string, reads the file, and then deserializes the contents into a `NetworkConfig`.
186    ///
187    /// # Arguments
188    ///
189    /// * `file` - A string representing the path to the file from which to load the `NetworkConfig`.
190    ///
191    /// # Returns
192    ///
193    /// This function returns a `Result` that contains a `NetworkConfig` if the file was successfully read and deserialized, or a `NetworkConfigError` if an error occurred.
194    ///
195    /// # Errors
196    ///
197    /// This function will return an error if the file cannot be read or if the contents cannot be deserialized into a `NetworkConfig`.
198    ///
199    /// # Examples
200    ///
201    /// ```ignore
202    /// # use hotshot_orchestrator::config::NetworkConfig;
203    /// // # use hotshot::traits::election::static_committee::StaticElectionConfig;
204    /// let file = "/path/to/my/config".to_string();
205    /// // NOTE: broken due to staticelectionconfig not being importable
206    /// // cannot import staticelectionconfig from hotshot without creating circular dependency
207    /// // making this work probably involves the `types` crate implementing a dummy
208    /// // electionconfigtype just to make this example work
209    /// let config = NetworkConfig::<TYPES, StaticElectionConfig>::from_file(file).unwrap();
210    /// ```
211    pub fn from_file(file: String) -> Result<Self, NetworkConfigError> {
212        // read from file
213        let data = match fs::read(file) {
214            Ok(data) => data,
215            Err(e) => {
216                return Err(NetworkConfigError::ReadFromFileError(e));
217            },
218        };
219
220        // deserialize
221        match serde_json::from_slice(&data) {
222            Ok(data) => Ok(data),
223            Err(e) => Err(NetworkConfigError::DeserializeError(e)),
224        }
225    }
226
227    /// Serializes the `NetworkConfig` and writes it to a file.
228    ///
229    /// 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.
230    ///
231    /// # Arguments
232    ///
233    /// * `file` - A string representing the path to the file where the `NetworkConfig` should be saved.
234    ///
235    /// # Returns
236    ///
237    /// 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.
238    ///
239    /// # Errors
240    ///
241    /// This function will return an error if the `NetworkConfig` cannot be serialized or if the file cannot be written.
242    ///
243    /// # Examples
244    ///
245    /// ```ignore
246    /// # use hotshot_orchestrator::config::NetworkConfig;
247    /// let file = "/path/to/my/config".to_string();
248    /// let config = NetworkConfig::from_file(file);
249    /// config.to_file(file).unwrap();
250    /// ```
251    pub fn to_file(&self, file: String) -> Result<(), NetworkConfigError> {
252        // ensure the directory containing the config file exists
253        if let Some(dir) = Path::new(&file).parent()
254            && let Err(e) = fs::create_dir_all(dir)
255        {
256            return Err(NetworkConfigError::FailedToCreatePath(e));
257        }
258
259        // serialize
260        let serialized = match serde_json::to_string_pretty(self) {
261            Ok(data) => data,
262            Err(e) => {
263                return Err(NetworkConfigError::SerializeError(e));
264            },
265        };
266
267        // write to file
268        match fs::write(file, serialized) {
269            Ok(()) => Ok(()),
270            Err(e) => Err(NetworkConfigError::WriteToFileError(e)),
271        }
272    }
273}
274
275impl<TYPES: NodeType> Default for NetworkConfig<TYPES> {
276    fn default() -> Self {
277        Self {
278            rounds: ORCHESTRATOR_DEFAULT_NUM_ROUNDS,
279            indexed_da: true,
280            transactions_per_round: ORCHESTRATOR_DEFAULT_TRANSACTIONS_PER_ROUND,
281            node_index: 0,
282            seed: [0u8; 32],
283            transaction_size: ORCHESTRATOR_DEFAULT_TRANSACTION_SIZE,
284            manual_start_password: None,
285            libp2p_config: None,
286            config: HotShotConfigFile::hotshot_config_5_nodes_10_da().into(),
287            key_type_name: std::any::type_name::<TYPES>().to_string(),
288            cdn_marshal_address: None,
289            combined_network_config: None,
290            next_view_timeout: 10,
291            view_sync_timeout: Duration::from_secs(2),
292            num_bootrap: 5,
293            builder_timeout: Duration::from_secs(10),
294            data_request_delay: Duration::from_millis(2500),
295            commit_sha: String::new(),
296            builder: BuilderType::default(),
297            random_builder: None,
298            public_keys: vec![],
299        }
300    }
301}
302
303/// a network config stored in a file
304#[serde_inline_default]
305#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
306#[serde(bound(deserialize = ""))]
307pub struct PublicKeysFile<TYPES: NodeType> {
308    /// The list of public keys that are allowed to connect to the orchestrator
309    ///
310    /// If nonempty, this list becomes the stake table and is used to determine DA membership (ignoring the node's request).
311    #[serde(default)]
312    pub public_keys: Vec<PeerConfigKeys<TYPES>>,
313}
314
315/// a network config stored in a file
316#[serde_inline_default]
317#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
318#[serde(bound(deserialize = ""))]
319pub struct NetworkConfigFile<TYPES: NodeType> {
320    /// number of views to run
321    #[serde_inline_default(ORCHESTRATOR_DEFAULT_NUM_ROUNDS)]
322    pub rounds: usize,
323    /// number of views to run
324    #[serde(default)]
325    pub indexed_da: bool,
326    /// number of transactions per view
327    #[serde_inline_default(ORCHESTRATOR_DEFAULT_TRANSACTIONS_PER_ROUND)]
328    pub transactions_per_round: usize,
329    /// password to have the orchestrator start the network,
330    /// regardless of the number of nodes connected.
331    #[serde(default)]
332    pub manual_start_password: Option<String>,
333    /// global index of node (for testing purposes a uid)
334    #[serde(default)]
335    pub node_index: u64,
336    /// unique seed (for randomness? TODO)
337    #[serde(default)]
338    pub seed: [u8; 32],
339    /// size of transactions
340    #[serde_inline_default(ORCHESTRATOR_DEFAULT_TRANSACTION_SIZE)]
341    pub transaction_size: usize,
342    /// the hotshot config file
343    #[serde(default = "HotShotConfigFile::hotshot_config_5_nodes_10_da")]
344    pub config: HotShotConfigFile<TYPES>,
345    /// The address of the Push CDN's "marshal", A.K.A. load balancer
346    #[serde(default)]
347    pub cdn_marshal_address: Option<String>,
348    /// combined network config
349    #[serde(default)]
350    pub combined_network_config: Option<CombinedNetworkConfig>,
351    /// builder to use
352    #[serde(default)]
353    pub builder: BuilderType,
354    /// random builder configuration
355    #[serde(default)]
356    pub random_builder: Option<RandomBuilderConfig>,
357    /// The list of public keys that are allowed to connect to the orchestrator
358    ///
359    /// If nonempty, this list becomes the stake table and is used to determine DA membership (ignoring the node's request).
360    #[serde(default)]
361    pub public_keys: Vec<PeerConfigKeys<TYPES>>,
362}
363
364impl<TYPES: NodeType> From<NetworkConfigFile<TYPES>> for NetworkConfig<TYPES> {
365    fn from(val: NetworkConfigFile<TYPES>) -> Self {
366        NetworkConfig {
367            rounds: val.rounds,
368            indexed_da: val.indexed_da,
369            transactions_per_round: val.transactions_per_round,
370            node_index: 0,
371            num_bootrap: val.config.num_bootstrap,
372            manual_start_password: val.manual_start_password,
373            next_view_timeout: val.config.next_view_timeout,
374            view_sync_timeout: val.config.view_sync_timeout,
375            builder_timeout: val.config.builder_timeout,
376            data_request_delay: val
377                .config
378                .data_request_delay
379                .unwrap_or(Duration::from_millis(REQUEST_DATA_DELAY)),
380            seed: val.seed,
381            transaction_size: val.transaction_size,
382            libp2p_config: Some(Libp2pConfig {
383                bootstrap_nodes: Vec::new(),
384            }),
385            config: val.config.into(),
386            key_type_name: std::any::type_name::<TYPES>().to_string(),
387            cdn_marshal_address: val.cdn_marshal_address,
388            combined_network_config: val.combined_network_config,
389            commit_sha: String::new(),
390            builder: val.builder,
391            random_builder: val.random_builder,
392            public_keys: val.public_keys,
393        }
394    }
395}