1use std::collections::BTreeSet;
4
5use exo_core::{Did, Hash256, PublicKey, Timestamp};
6use serde::{Deserialize, Serialize};
7
8use crate::{Result, RootError};
9
10pub const ROOT_GENESIS_THRESHOLD: u16 = 7;
12
13pub const ROOT_GENESIS_SIGNERS: u16 = 13;
15
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
18pub struct CertifierContact {
19 pub did: Did,
21 pub frost_identifier: u16,
23 pub signing_public_key: PublicKey,
25 pub transport_public_key: [u8; 32],
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct GenesisCeremonyConfig {
32 pub ceremony_id: String,
34 pub network_id: String,
36 pub repo_commit: String,
38 pub constitution_hash: Hash256,
40 pub threshold: u16,
42 pub max_signers: u16,
44 pub created_at: Timestamp,
46 pub certifiers: Vec<CertifierContact>,
48 pub signing_set: Vec<u16>,
53}
54
55impl GenesisCeremonyConfig {
56 pub fn validate(&self) -> Result<()> {
58 if self.threshold != ROOT_GENESIS_THRESHOLD {
59 return Err(RootError::InvalidConfig {
60 reason: format!("threshold must be {ROOT_GENESIS_THRESHOLD}"),
61 });
62 }
63 if self.max_signers != ROOT_GENESIS_SIGNERS {
64 return Err(RootError::InvalidConfig {
65 reason: format!("max_signers must be {ROOT_GENESIS_SIGNERS}"),
66 });
67 }
68 if self.certifiers.len() != usize::from(ROOT_GENESIS_SIGNERS) {
69 return Err(RootError::InvalidConfig {
70 reason: format!("roster must contain {ROOT_GENESIS_SIGNERS} certifiers"),
71 });
72 }
73 if self.ceremony_id.trim().is_empty() {
74 return Err(RootError::InvalidConfig {
75 reason: "ceremony_id must not be empty".to_owned(),
76 });
77 }
78 if self.network_id.trim().is_empty() {
79 return Err(RootError::InvalidConfig {
80 reason: "network_id must not be empty".to_owned(),
81 });
82 }
83 if self.repo_commit.len() != 40 || !self.repo_commit.bytes().all(|b| b.is_ascii_hexdigit())
84 {
85 return Err(RootError::InvalidConfig {
86 reason: "repo_commit must be a 40-character hex commit".to_owned(),
87 });
88 }
89
90 let mut dids = BTreeSet::new();
91 let mut frost_ids = BTreeSet::new();
92 let mut signing_keys = BTreeSet::new();
93 let mut transport_keys = BTreeSet::new();
94 for certifier in &self.certifiers {
95 if certifier.frost_identifier == 0 || certifier.frost_identifier > ROOT_GENESIS_SIGNERS
96 {
97 return Err(RootError::InvalidConfig {
98 reason: format!(
99 "frost_identifier {} is outside 1..={ROOT_GENESIS_SIGNERS}",
100 certifier.frost_identifier
101 ),
102 });
103 }
104 if !dids.insert(certifier.did.clone()) {
105 return Err(RootError::InvalidConfig {
106 reason: format!("duplicate certifier DID {}", certifier.did),
107 });
108 }
109 if !frost_ids.insert(certifier.frost_identifier) {
110 return Err(RootError::InvalidConfig {
111 reason: format!("duplicate FROST identifier {}", certifier.frost_identifier),
112 });
113 }
114 if !signing_keys.insert(certifier.signing_public_key) {
115 return Err(RootError::InvalidConfig {
116 reason: "duplicate signing public key".to_owned(),
117 });
118 }
119 if !transport_keys.insert(certifier.transport_public_key) {
120 return Err(RootError::InvalidConfig {
121 reason: "duplicate transport public key".to_owned(),
122 });
123 }
124 }
125
126 self.validate_signing_set()?;
127
128 Ok(())
129 }
130
131 fn validate_signing_set(&self) -> Result<()> {
133 if self.signing_set.len() != usize::from(self.threshold) {
134 return Err(RootError::InvalidConfig {
135 reason: format!(
136 "signing_set must contain exactly {} signers",
137 self.threshold
138 ),
139 });
140 }
141 let mut declared = BTreeSet::new();
142 for identifier in &self.signing_set {
143 if self.certifier_by_identifier(*identifier).is_none() {
144 return Err(RootError::InvalidConfig {
145 reason: format!("signing_set member {identifier} is not rostered"),
146 });
147 }
148 if !declared.insert(*identifier) {
149 return Err(RootError::InvalidConfig {
150 reason: format!("duplicate signing_set member {identifier}"),
151 });
152 }
153 }
154 Ok(())
155 }
156
157 pub fn validate_signing_selection(&self, submitted: &BTreeSet<u16>) -> Result<()> {
162 if submitted.len() != usize::from(self.threshold) {
163 return Err(RootError::InvalidConfig {
164 reason: format!(
165 "signing selection must contain exactly {} signers",
166 self.threshold
167 ),
168 });
169 }
170 let expected: BTreeSet<u16> = self.signing_set.iter().copied().collect();
171 if &expected != submitted {
172 return Err(RootError::InvalidConfig {
173 reason: "signing selection must exactly match the predeclared signing_set"
174 .to_owned(),
175 });
176 }
177 Ok(())
178 }
179
180 #[must_use]
182 pub fn certifier_by_did(&self, did: &Did) -> Option<&CertifierContact> {
183 self.certifiers
184 .iter()
185 .find(|certifier| &certifier.did == did)
186 }
187
188 #[must_use]
190 pub fn certifier_by_identifier(&self, identifier: u16) -> Option<&CertifierContact> {
191 self.certifiers
192 .iter()
193 .find(|certifier| certifier.frost_identifier == identifier)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use std::collections::BTreeSet;
200
201 use exo_core::{Did, Hash256, PublicKey, Timestamp};
202
203 use super::*;
204
205 fn config() -> GenesisCeremonyConfig {
206 let certifiers = (1..=ROOT_GENESIS_SIGNERS)
207 .map(|index| {
208 let byte = u8::try_from(index).expect("index fits");
209 CertifierContact {
210 did: Did::new(&format!("did:exo:ceremony-unit-{index:02}")).expect("did"),
211 frost_identifier: index,
212 signing_public_key: PublicKey::from_bytes([byte; 32]),
213 transport_public_key: [byte; 32],
214 }
215 })
216 .collect();
217 GenesisCeremonyConfig {
218 ceremony_id: "ceremony-unit".into(),
219 network_id: "exochain-test".into(),
220 repo_commit: "d8927686a34bdc28ba36d53938f665685d2c4c04".into(),
221 constitution_hash: Hash256::digest(b"constitution"),
222 threshold: ROOT_GENESIS_THRESHOLD,
223 max_signers: ROOT_GENESIS_SIGNERS,
224 created_at: Timestamp::new(1, 0),
225 certifiers,
226 signing_set: (1..=7).collect(),
227 }
228 }
229
230 fn selection(ids: &[u16]) -> BTreeSet<u16> {
231 ids.iter().copied().collect()
232 }
233
234 #[test]
235 fn valid_config_with_signing_set_validates() {
236 config().validate().expect("config validates");
237 }
238
239 #[test]
240 fn validate_rejects_malformed_signing_set() {
241 let mut wrong_len = config();
242 wrong_len.signing_set = (1..=6).collect();
243 assert!(wrong_len.validate().is_err());
244
245 let mut unrostered_primary = config();
246 unrostered_primary.signing_set = vec![1, 2, 3, 4, 5, 6, 99];
247 assert!(unrostered_primary.validate().is_err());
248
249 let mut duplicate_primary = config();
250 duplicate_primary.signing_set = vec![1, 2, 3, 4, 5, 6, 6];
251 assert!(duplicate_primary.validate().is_err());
252 }
253
254 #[test]
255 fn validate_signing_selection_accepts_only_declared_set() {
256 let config = config();
257 config
258 .validate_signing_selection(&selection(&[1, 2, 3, 4, 5, 6, 7]))
259 .expect("declared set accepted");
260 }
261
262 #[test]
263 fn validate_signing_selection_rejects_wrong_size_unknown_and_substitution() {
264 let config = config();
265 assert!(
267 config
268 .validate_signing_selection(&selection(&[1, 2, 3, 4, 5, 6, 7, 8]))
269 .is_err()
270 );
271 assert!(
273 config
274 .validate_signing_selection(&selection(&[1, 2, 3, 4, 5, 6, 99]))
275 .is_err()
276 );
277 assert!(
279 config
280 .validate_signing_selection(&selection(&[1, 2, 3, 4, 5, 6, 8]))
281 .is_err()
282 );
283 }
284}