1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct GenesisValidator {
16 pub name: String,
18
19 pub pubkey: String,
21
22 pub weight: u64,
24
25 pub addr: String,
27}
28
29impl GenesisValidator {
30 pub fn into_validator(self) -> Result<Validator> {
32 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#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct GenesisRepository {
57 pub owner: String,
59
60 pub name: String,
62
63 pub description: String,
65
66 pub default_branch: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ConsensusParams {
73 pub block_time_ms: u64,
75
76 pub max_txs_per_block: usize,
78
79 pub max_block_size: usize,
81
82 pub view_timeout_multiplier: f64,
84
85 pub min_validators: usize,
87
88 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, view_timeout_multiplier: 2.0,
99 min_validators: 4,
100 max_validators: 100,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct Genesis {
108 pub chain_id: String,
110
111 pub timestamp: u64,
113
114 pub validators: Vec<GenesisValidator>,
116
117 #[serde(default)]
119 pub repositories: Vec<GenesisRepository>,
120
121 #[serde(default)]
123 pub consensus: ConsensusParams,
124}
125
126impl Genesis {
127 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 pub fn with_validator(mut self, validator: GenesisValidator) -> Self {
140 self.validators.push(validator);
141 self
142 }
143
144 pub fn with_consensus_params(mut self, params: ConsensusParams) -> Self {
146 self.consensus = params;
147 self
148 }
149
150 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 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 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 for v in &self.validators {
191 let _ = v.clone().into_validator()?;
192 }
193
194 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 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 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 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
256pub 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 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}