Skip to main content

iota_swarm_config/
network_config.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::path::Path;
6
7use anyhow::{Context, Result, bail, ensure};
8use fastcrypto::encoding::{Base64, Encoding};
9use iota_config::{Config, IOTA_NETWORK_CONFIG, NodeConfig, genesis, node};
10use iota_multiaddr::Multiaddr;
11use iota_sdk_crypto::ToFromBytes as _;
12use iota_types::{committee::CommitteeWithNetworkMetadata, crypto::AccountPrivateKey};
13use serde::{Deserialize, Deserializer, Serialize, Serializer};
14use serde_with::{DeserializeAs, SerializeAs, serde_as};
15
16use crate::{genesis_config::GenesisConfig, node_config_builder::ValidatorConfigBuilder};
17
18/// Serializes an account key as a base64 string of the raw 32-byte ed25519
19/// private key.
20struct AccountPrivateKeyBase64;
21
22impl SerializeAs<AccountPrivateKey> for AccountPrivateKeyBase64 {
23    fn serialize_as<S: Serializer>(key: &AccountPrivateKey, s: S) -> Result<S::Ok, S::Error> {
24        Base64::encode(key.to_bytes()).serialize(s)
25    }
26}
27
28impl<'de> DeserializeAs<'de, AccountPrivateKey> for AccountPrivateKeyBase64 {
29    fn deserialize_as<D: Deserializer<'de>>(d: D) -> Result<AccountPrivateKey, D::Error> {
30        let bytes = Base64::decode(&String::deserialize(d)?).map_err(serde::de::Error::custom)?;
31        AccountPrivateKey::from_bytes(&bytes).map_err(serde::de::Error::custom)
32    }
33}
34
35/// This is a config that is used for testing or local use as it contains the
36/// config and keys for all validators
37#[serde_as]
38#[derive(Debug, Deserialize, Serialize)]
39pub struct NetworkConfig {
40    pub validator_configs: Vec<NodeConfig>,
41    #[serde_as(as = "Vec<AccountPrivateKeyBase64>")]
42    pub account_keys: Vec<AccountPrivateKey>,
43    pub genesis: genesis::Genesis,
44}
45
46impl Config for NetworkConfig {}
47
48impl NetworkConfig {
49    pub fn validator_configs(&self) -> &[NodeConfig] {
50        &self.validator_configs
51    }
52
53    pub fn net_addresses(&self) -> Vec<Multiaddr> {
54        self.genesis
55            .committee_with_network()
56            .validators()
57            .values()
58            .map(|(_, n)| n.network_address.clone())
59            .collect()
60    }
61
62    pub fn committee_with_network(&self) -> CommitteeWithNetworkMetadata {
63        self.genesis.committee_with_network()
64    }
65
66    pub fn into_validator_configs(self) -> Vec<NodeConfig> {
67        self.validator_configs
68    }
69
70    /// Retrieve genesis information that might be present in the configured
71    /// validators.
72    pub fn get_validator_genesis(&self) -> Option<&node::Genesis> {
73        self.validator_configs
74            .first()
75            .as_ref()
76            .map(|validator| &validator.genesis)
77    }
78}
79
80/// What `iota-localnet` writes to `network.yaml`: everything the node configs
81/// of a local network are derived from, and the version of the format they are
82/// written in.
83///
84/// The genesis blob is not derived from this. It is persisted beside it and
85/// only read.
86#[serde_as]
87#[derive(Deserialize, Serialize)]
88pub struct PersistedNetworkConfig {
89    /// The version of this file's format. A file without one predates the
90    /// field and cannot be read.
91    pub version: u32,
92    pub genesis_config: GenesisConfig,
93    #[serde_as(as = "Vec<AccountPrivateKeyBase64>")]
94    pub account_keys: Vec<AccountPrivateKey>,
95}
96
97impl Config for PersistedNetworkConfig {}
98
99/// Reads only the format version, so that a file this build cannot read is
100/// rejected before its other fields are.
101#[derive(Deserialize)]
102struct NetworkConfigFormatVersion {
103    #[serde(default)]
104    version: Option<u32>,
105}
106
107impl PersistedNetworkConfig {
108    /// The format version this build writes and reads.
109    pub const VERSION: u32 = 1;
110
111    /// Read the network config from `network.yaml` in `config_directory`.
112    ///
113    /// # Errors
114    ///
115    /// - The file is missing or unreadable.
116    /// - It was written in a format version this build does not read, which
117    ///   includes every file written before the version field existed.
118    pub fn read(config_directory: &Path) -> Result<Self> {
119        let path = config_directory.join(IOTA_NETWORK_CONFIG);
120        let contents = std::fs::read_to_string(&path)
121            .with_context(|| format!("cannot open the IOTA network config file at {path:?}"))?;
122        let format_version: NetworkConfigFormatVersion = serde_yaml::from_str(&contents)
123            .with_context(|| format!("cannot read the IOTA network config file at {path:?}"))?;
124        if format_version.version != Some(Self::VERSION) {
125            // `genesis --force` deletes the directory, which is the wrong
126            // advice for a file a newer build wrote.
127            if format_version.version > Some(Self::VERSION) {
128                bail!(
129                    "the configuration in {} was created by a newer version of iota-localnet and \
130                     cannot be read. Update iota-localnet.",
131                    config_directory.display()
132                );
133            }
134            bail!(
135                "the configuration in {} was created by an older version of iota-localnet and \
136                 cannot be read. Re-create it with `iota-localnet genesis --force`.",
137                config_directory.display()
138            );
139        }
140        serde_yaml::from_str(&contents)
141            .with_context(|| format!("cannot read the IOTA network config file at {path:?}"))
142    }
143
144    /// Derive the node config of every validator of this network, attaching
145    /// `genesis` to each rather than building a genesis from the genesis
146    /// config.
147    ///
148    /// # Errors
149    ///
150    /// - The network has no validator.
151    /// - `genesis` cannot be read.
152    pub fn into_network_config(
153        self,
154        config_directory: &Path,
155        genesis: node::Genesis,
156    ) -> Result<NetworkConfig> {
157        let validators = self
158            .genesis_config
159            .validator_config_info
160            .unwrap_or_default();
161        ensure!(
162            !validators.is_empty(),
163            "the IOTA network config must contain at least one validator"
164        );
165        let validator_configs = validators
166            .into_iter()
167            .map(|validator| {
168                let mut config = ValidatorConfigBuilder::new()
169                    .with_config_directory(config_directory.to_path_buf())
170                    .build_without_genesis(validator);
171                config.genesis = genesis.clone();
172                config
173            })
174            .collect();
175        Ok(NetworkConfig {
176            validator_configs,
177            account_keys: self.account_keys,
178            genesis: genesis.genesis()?.clone(),
179        })
180    }
181}
182
183/// This is the light version of [`NetworkConfig`] that does not
184/// contain the entire [`genesis::Genesis`].
185#[serde_as]
186#[derive(Debug, Deserialize, Serialize)]
187pub struct NetworkConfigLight {
188    pub validator_configs: Vec<NodeConfig>,
189    #[serde_as(as = "Vec<AccountPrivateKeyBase64>")]
190    pub account_keys: Vec<AccountPrivateKey>,
191    pub committee_with_network: CommitteeWithNetworkMetadata,
192}
193
194impl Config for NetworkConfigLight {}
195
196impl NetworkConfigLight {
197    pub fn new(
198        validator_configs: Vec<NodeConfig>,
199        account_keys: Vec<AccountPrivateKey>,
200        genesis: &genesis::Genesis,
201    ) -> Self {
202        Self {
203            validator_configs,
204            account_keys,
205            committee_with_network: genesis.committee_with_network(),
206        }
207    }
208
209    pub fn validator_configs(&self) -> &[NodeConfig] {
210        &self.validator_configs
211    }
212
213    pub fn net_addresses(&self) -> Vec<Multiaddr> {
214        self.committee_with_network
215            .validators()
216            .values()
217            .map(|(_, n)| n.network_address.clone())
218            .collect()
219    }
220
221    pub fn into_validator_configs(self) -> Vec<NodeConfig> {
222        self.validator_configs
223    }
224
225    /// Retrieve genesis information that might be present in the configured
226    /// validators.
227    pub fn get_validator_genesis(&self) -> Option<&node::Genesis> {
228        self.validator_configs
229            .first()
230            .as_ref()
231            .map(|validator| &validator.genesis)
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    /// A `network.yaml` a hand edit left without validators is refused here,
240    /// rather than deeper in the launch of a network that has no committee.
241    #[test]
242    fn a_persisted_config_without_validators_is_rejected() {
243        let directory = tempfile::tempdir().unwrap();
244        let persisted = PersistedNetworkConfig {
245            version: PersistedNetworkConfig::VERSION,
246            genesis_config: GenesisConfig::for_local_testing(),
247            account_keys: vec![],
248        };
249
250        let err = persisted
251            .into_network_config(directory.path(), node::Genesis::new_empty())
252            .unwrap_err();
253
254        assert!(err.to_string().contains("at least one validator"), "{err}");
255    }
256}