guts_consensus/
genesis.rs

1//! Genesis configuration for the consensus network.
2//!
3//! The genesis file defines the initial state of the network including
4//! the initial validator set and consensus parameters.
5
6use crate::error::{ConsensusError, Result};
7use crate::transaction::SerializablePublicKey;
8use crate::validator::{Validator, ValidatorConfig, ValidatorSet};
9use serde::{Deserialize, Serialize};
10use std::net::SocketAddr;
11use std::path::Path;
12
13/// Genesis configuration for a validator.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct GenesisValidator {
16    /// Human-readable name.
17    pub name: String,
18
19    /// Public key (hex-encoded).
20    pub pubkey: String,
21
22    /// Voting weight.
23    pub weight: u64,
24
25    /// Network address.
26    pub addr: String,
27}
28
29impl GenesisValidator {
30    /// Converts to a `Validator`.
31    pub fn into_validator(self) -> Result<Validator> {
32        // Validate the public key format
33        let pubkey_bytes =
34            hex::decode(&self.pubkey).map_err(|e| ConsensusError::InvalidGenesis(e.to_string()))?;
35
36        if pubkey_bytes.len() != 32 {
37            return Err(ConsensusError::InvalidGenesis(format!(
38                "invalid public key length: expected 32 bytes, got {}",
39                pubkey_bytes.len()
40            )));
41        }
42
43        let pubkey = SerializablePublicKey::from_hex(&self.pubkey);
44
45        let addr: SocketAddr = self
46            .addr
47            .parse()
48            .map_err(|e| ConsensusError::InvalidGenesis(format!("invalid address: {}", e)))?;
49
50        Ok(Validator::new(pubkey, self.name, self.weight, addr))
51    }
52}
53
54/// Genesis repository (for testnet seeding).
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct GenesisRepository {
57    /// Owner username.
58    pub owner: String,
59
60    /// Repository name.
61    pub name: String,
62
63    /// Description.
64    pub description: String,
65
66    /// Default branch.
67    pub default_branch: String,
68}
69
70/// Consensus parameters from genesis.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ConsensusParams {
73    /// Target block time in milliseconds.
74    pub block_time_ms: u64,
75
76    /// Maximum transactions per block.
77    pub max_txs_per_block: usize,
78
79    /// Maximum block size in bytes.
80    pub max_block_size: usize,
81
82    /// View timeout multiplier.
83    pub view_timeout_multiplier: f64,
84
85    /// Minimum validators.
86    pub min_validators: usize,
87
88    /// Maximum validators.
89    pub max_validators: usize,
90}
91
92impl Default for ConsensusParams {
93    fn default() -> Self {
94        Self {
95            block_time_ms: 2000,
96            max_txs_per_block: 1000,
97            max_block_size: 10 * 1024 * 1024, // 10 MB
98            view_timeout_multiplier: 2.0,
99            min_validators: 4,
100            max_validators: 100,
101        }
102    }
103}
104
105/// Complete genesis configuration.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct Genesis {
108    /// Network identifier (chain ID).
109    pub chain_id: String,
110
111    /// Genesis timestamp (unix milliseconds).
112    pub timestamp: u64,
113
114    /// Initial validators.
115    pub validators: Vec<GenesisValidator>,
116
117    /// Initial repositories (optional, for testnet).
118    #[serde(default)]
119    pub repositories: Vec<GenesisRepository>,
120
121    /// Consensus parameters.
122    #[serde(default)]
123    pub consensus: ConsensusParams,
124}
125
126impl Genesis {
127    /// Creates a new genesis configuration.
128    pub fn new(chain_id: impl Into<String>, timestamp: u64) -> Self {
129        Self {
130            chain_id: chain_id.into(),
131            timestamp,
132            validators: Vec::new(),
133            repositories: Vec::new(),
134            consensus: ConsensusParams::default(),
135        }
136    }
137
138    /// Adds a validator to the genesis.
139    pub fn with_validator(mut self, validator: GenesisValidator) -> Self {
140        self.validators.push(validator);
141        self
142    }
143
144    /// Sets the consensus parameters.
145    pub fn with_consensus_params(mut self, params: ConsensusParams) -> Self {
146        self.consensus = params;
147        self
148    }
149
150    /// Loads genesis from a JSON file.
151    pub fn load_json(path: impl AsRef<Path>) -> Result<Self> {
152        let content = std::fs::read_to_string(path.as_ref())
153            .map_err(|e| ConsensusError::InvalidGenesis(format!("failed to read file: {}", e)))?;
154
155        let genesis: Genesis = serde_json::from_str(&content)?;
156        genesis.validate()?;
157        Ok(genesis)
158    }
159
160    /// Loads genesis from a YAML file.
161    pub fn load_yaml(path: impl AsRef<Path>) -> Result<Self> {
162        let content = std::fs::read_to_string(path.as_ref())
163            .map_err(|e| ConsensusError::InvalidGenesis(format!("failed to read file: {}", e)))?;
164
165        let genesis: Genesis = serde_yaml::from_str(&content)
166            .map_err(|e| ConsensusError::InvalidGenesis(e.to_string()))?;
167        genesis.validate()?;
168        Ok(genesis)
169    }
170
171    /// Validates the genesis configuration.
172    pub fn validate(&self) -> Result<()> {
173        if self.chain_id.is_empty() {
174            return Err(ConsensusError::InvalidGenesis("chain_id is empty".into()));
175        }
176
177        if self.validators.is_empty() {
178            return Err(ConsensusError::InvalidGenesis("no validators".into()));
179        }
180
181        if self.validators.len() < self.consensus.min_validators {
182            return Err(ConsensusError::InvalidGenesis(format!(
183                "need at least {} validators for BFT, got {}",
184                self.consensus.min_validators,
185                self.validators.len()
186            )));
187        }
188
189        // Verify all public keys are valid
190        for v in &self.validators {
191            let _ = v.clone().into_validator()?;
192        }
193
194        // Check for duplicate names or pubkeys
195        let mut seen_names = std::collections::HashSet::new();
196        let mut seen_pubkeys = std::collections::HashSet::new();
197
198        for v in &self.validators {
199            if !seen_names.insert(&v.name) {
200                return Err(ConsensusError::InvalidGenesis(format!(
201                    "duplicate validator name: {}",
202                    v.name
203                )));
204            }
205            if !seen_pubkeys.insert(&v.pubkey) {
206                return Err(ConsensusError::InvalidGenesis(format!(
207                    "duplicate validator pubkey: {}",
208                    v.pubkey
209                )));
210            }
211        }
212
213        Ok(())
214    }
215
216    /// Converts to a ValidatorSet.
217    pub fn into_validator_set(self) -> Result<ValidatorSet> {
218        let validators: Result<Vec<_>> = self
219            .validators
220            .into_iter()
221            .map(|gv| gv.into_validator())
222            .collect();
223
224        let config = ValidatorConfig {
225            min_validators: self.consensus.min_validators,
226            max_validators: self.consensus.max_validators,
227            quorum_threshold: 2.0 / 3.0,
228            block_time_ms: self.consensus.block_time_ms,
229        };
230
231        ValidatorSet::new(validators?, 0, config)
232    }
233
234    /// Saves genesis to a JSON file.
235    pub fn save_json(&self, path: impl AsRef<Path>) -> Result<()> {
236        let content = serde_json::to_string_pretty(self).map_err(ConsensusError::from)?;
237
238        std::fs::write(path.as_ref(), content)
239            .map_err(|e| ConsensusError::InvalidGenesis(format!("failed to write file: {}", e)))?;
240
241        Ok(())
242    }
243
244    /// Saves genesis to a YAML file.
245    pub fn save_yaml(&self, path: impl AsRef<Path>) -> Result<()> {
246        let content = serde_yaml::to_string(self)
247            .map_err(|e| ConsensusError::InvalidGenesis(e.to_string()))?;
248
249        std::fs::write(path.as_ref(), content)
250            .map_err(|e| ConsensusError::InvalidGenesis(format!("failed to write file: {}", e)))?;
251
252        Ok(())
253    }
254}
255
256/// Helper to generate a devnet genesis with test validators.
257pub fn generate_devnet_genesis(validator_count: usize) -> Genesis {
258    use commonware_cryptography::{ed25519, PrivateKeyExt, Signer};
259
260    let validators: Vec<GenesisValidator> = (0..validator_count as u64)
261        .map(|i| {
262            let key = ed25519::PrivateKey::from_seed(i);
263            let pubkey = hex::encode(key.public_key().as_ref());
264
265            GenesisValidator {
266                name: format!("validator-{}", i + 1),
267                pubkey,
268                weight: 100,
269                addr: format!("127.0.0.1:{}", 9000 + i),
270            }
271        })
272        .collect();
273
274    Genesis {
275        chain_id: "guts-devnet".into(),
276        timestamp: std::time::SystemTime::now()
277            .duration_since(std::time::UNIX_EPOCH)
278            .unwrap()
279            .as_millis() as u64,
280        validators,
281        repositories: vec![],
282        consensus: ConsensusParams::default(),
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_genesis_validation() {
292        let genesis = generate_devnet_genesis(4);
293        assert!(genesis.validate().is_ok());
294    }
295
296    #[test]
297    fn test_genesis_too_few_validators() {
298        let genesis = generate_devnet_genesis(2);
299        assert!(matches!(
300            genesis.validate(),
301            Err(ConsensusError::InvalidGenesis(_))
302        ));
303    }
304
305    #[test]
306    fn test_genesis_to_validator_set() {
307        let genesis = generate_devnet_genesis(4);
308        let set = genesis.into_validator_set().unwrap();
309
310        assert_eq!(set.len(), 4);
311        assert_eq!(set.epoch(), 0);
312    }
313
314    #[test]
315    fn test_genesis_serialization() {
316        let genesis = generate_devnet_genesis(4);
317
318        // JSON roundtrip
319        let json = serde_json::to_string(&genesis).unwrap();
320        let parsed: Genesis = serde_json::from_str(&json).unwrap();
321        assert_eq!(parsed.chain_id, genesis.chain_id);
322        assert_eq!(parsed.validators.len(), 4);
323    }
324
325    #[test]
326    fn test_genesis_duplicate_name() {
327        let mut genesis = generate_devnet_genesis(4);
328        genesis.validators[1].name = genesis.validators[0].name.clone();
329
330        assert!(matches!(
331            genesis.validate(),
332            Err(ConsensusError::InvalidGenesis(msg)) if msg.contains("duplicate validator name")
333        ));
334    }
335
336    #[test]
337    fn test_genesis_duplicate_pubkey() {
338        let mut genesis = generate_devnet_genesis(4);
339        genesis.validators[1].pubkey = genesis.validators[0].pubkey.clone();
340
341        assert!(matches!(
342            genesis.validate(),
343            Err(ConsensusError::InvalidGenesis(msg)) if msg.contains("duplicate validator pubkey")
344        ));
345    }
346}