1use crate::core::{
5 DEFAULT_MAX_RETRIES, ElectionDecision, StewardList, StewardListConfig, StewardListEvent,
6 StewardListPlugin, error::CoreError,
7};
8
9#[derive(Debug)]
10pub struct DeterministicStewardList {
11 list: Option<StewardList>,
12 config: StewardListConfig,
13 conversation_id: Vec<u8>,
14 retry_round: u32,
15 max_retries: u32,
16}
17
18impl DeterministicStewardList {
19 pub fn empty(conversation_id: impl Into<Vec<u8>>, config: StewardListConfig) -> Self {
22 Self {
23 list: None,
24 config,
25 conversation_id: conversation_id.into(),
26 retry_round: 0,
27 max_retries: DEFAULT_MAX_RETRIES,
28 }
29 }
30
31 pub fn with_creator(
34 conversation_id: impl Into<Vec<u8>>,
35 creator_identity: Vec<u8>,
36 config: StewardListConfig,
37 ) -> Result<Self, CoreError> {
38 let conversation_id = conversation_id.into();
39 let list = StewardList::generate(
40 0,
41 &conversation_id,
42 &[creator_identity],
43 1,
44 config.clone(),
45 0,
46 )?;
47 Ok(Self {
48 list: Some(list),
49 config,
50 conversation_id,
51 retry_round: 0,
52 max_retries: DEFAULT_MAX_RETRIES,
53 })
54 }
55}
56
57impl StewardListPlugin for DeterministicStewardList {
58 fn config(&self) -> &StewardListConfig {
59 &self.config
60 }
61
62 fn set_config(&mut self, config: StewardListConfig) {
63 self.config = config;
64 }
65
66 fn current_list(&self) -> Option<&StewardList> {
67 self.list.as_ref()
68 }
69
70 fn election_epoch(&self) -> Option<u64> {
71 self.list.as_ref().map(|l| l.election_epoch())
72 }
73
74 fn retry_round(&self) -> u32 {
75 self.retry_round
76 }
77
78 fn max_retries(&self) -> u32 {
79 self.max_retries
80 }
81
82 fn set_max_retries(&mut self, max: u32) {
83 self.max_retries = max;
84 }
85
86 fn is_steward(&self, identity: &[u8]) -> bool {
87 self.list.as_ref().is_some_and(|l| l.contains(identity))
88 }
89
90 fn is_exhausted(&self, epoch: u64) -> bool {
91 self.list.as_ref().is_some_and(|l| l.is_exhausted(epoch))
92 }
93
94 fn epoch_steward<F: Fn(&[u8]) -> bool>(&self, epoch: u64, eligible: F) -> Option<&[u8]> {
95 self.list
96 .as_ref()
97 .and_then(|l| l.live_steward_from(epoch, 0, eligible))
98 }
99
100 fn epoch_and_backup<F: Fn(&[u8]) -> bool>(
101 &self,
102 epoch: u64,
103 eligible: F,
104 ) -> (Option<&[u8]>, Option<&[u8]>) {
105 match self.list.as_ref() {
106 Some(l) => l.live_epoch_and_backup(epoch, eligible),
107 None => (None, None),
108 }
109 }
110
111 fn steward_members<F: Fn(&[u8]) -> bool>(&self, eligible: F) -> Vec<Vec<u8>> {
112 self.list
113 .as_ref()
114 .map(|l| {
115 l.members()
116 .iter()
117 .filter(|m| eligible(m))
118 .cloned()
119 .collect()
120 })
121 .unwrap_or_default()
122 }
123
124 fn election_proposer<F: Fn(&[u8]) -> bool>(&self, eligible: F) -> Option<&[u8]> {
125 self.list
128 .as_ref()
129 .and_then(|l| l.live_steward_from(l.election_epoch(), 0, eligible))
130 }
131
132 fn install_list(
133 &mut self,
134 epoch: u64,
135 candidate_pool: &[Vec<u8>],
136 sn: usize,
137 retry_round: u32,
138 ) -> Result<Vec<StewardListEvent>, CoreError> {
139 let list = StewardList::generate(
140 epoch,
141 &self.conversation_id,
142 candidate_pool,
143 sn,
144 self.config.clone(),
145 retry_round,
146 )?;
147 let len = list.len();
148 self.list = Some(list);
149 Ok(vec![StewardListEvent::ListInstalled {
150 epoch,
151 retry_round,
152 len,
153 }])
154 }
155
156 fn validate_proposed(
157 &self,
158 proposed: &[Vec<u8>],
159 epoch: u64,
160 candidate_pool: &[Vec<u8>],
161 retry_round: u32,
162 ) -> Result<bool, CoreError> {
163 StewardList::validate(
164 proposed,
165 epoch,
166 &self.conversation_id,
167 candidate_pool,
168 &self.config,
169 retry_round,
170 )
171 }
172
173 fn propose_election<F: Fn(&[u8]) -> bool>(
174 &self,
175 epoch: u64,
176 candidate_pool: &[Vec<u8>],
177 self_identity: &[u8],
178 eligible: F,
179 recovery: bool,
180 ) -> Result<ElectionDecision, CoreError> {
181 if !recovery && !self.is_exhausted(epoch) {
182 return Ok(ElectionDecision::Skip("steward list not exhausted"));
183 }
184 let is_authorized = self
185 .election_proposer(&eligible)
186 .is_some_and(|proposer| proposer == self_identity);
187 if !is_authorized {
188 return Ok(ElectionDecision::Skip("not the responsible proposer"));
189 }
190 if candidate_pool.is_empty() {
191 return Ok(ElectionDecision::Skip(
192 "no eligible candidates after filter",
193 ));
194 }
195
196 let retry_round = self.retry_round();
197 let sn = self.config.compute_list_size(candidate_pool.len());
198 let list = StewardList::generate(
199 epoch,
200 &self.conversation_id,
201 candidate_pool,
202 sn,
203 self.config.clone(),
204 retry_round,
205 )?;
206 Ok(ElectionDecision::Proposed {
207 proposed_stewards: list.members().to_vec(),
208 election_epoch: epoch,
209 retry_round,
210 })
211 }
212
213 fn maybe_auto_fill(
214 &mut self,
215 epoch: u64,
216 members: &[Vec<u8>],
217 ) -> Result<Vec<StewardListEvent>, CoreError> {
218 if members.len() >= self.config.sn_min {
223 return Ok(Vec::new());
224 }
225 let sn = self.config.compute_list_size(members.len());
226 self.install_list(epoch, members, sn, 0)
227 }
228
229 fn bump_retry(&mut self) -> Vec<StewardListEvent> {
230 self.retry_round = self.retry_round.saturating_add(1);
231 if self.retry_round > self.max_retries {
232 vec![StewardListEvent::RetryExhausted {
233 round: self.retry_round,
234 max: self.max_retries,
235 }]
236 } else {
237 Vec::new()
238 }
239 }
240
241 fn reset_retry(&mut self) {
242 self.retry_round = 0;
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 fn member(id: u8) -> Vec<u8> {
251 vec![id; 20]
252 }
253
254 fn members(ids: &[u8]) -> Vec<Vec<u8>> {
255 ids.iter().map(|&id| member(id)).collect()
256 }
257
258 fn config() -> StewardListConfig {
259 StewardListConfig::new(1, 5).unwrap()
260 }
261
262 #[test]
263 fn empty_plugin_has_no_list() {
264 let p = DeterministicStewardList::empty(b"g".to_vec(), config());
265 assert!(p.current_list().is_none());
266 assert!(!p.is_steward(&member(1)));
267 assert!(!p.is_exhausted(0));
268 assert_eq!(p.epoch_steward(0, |_: &[u8]| true), None);
269 assert_eq!(p.election_epoch(), None);
270 }
271
272 #[test]
273 fn creator_bootstrap_makes_creator_a_steward() {
274 let creator = member(1);
275 let p = DeterministicStewardList::with_creator(b"g".to_vec(), creator.clone(), config())
276 .unwrap();
277 assert!(p.is_steward(&creator));
278 assert_eq!(p.election_epoch(), Some(0));
279 assert_eq!(p.current_list().unwrap().len(), 1);
280 }
281
282 #[test]
283 fn install_emits_list_installed_event() {
284 let mut p = DeterministicStewardList::empty(b"g".to_vec(), config());
285 let mems = members(&[1, 2, 3]);
286 let events = p.install_list(0, &mems, 3, 0).unwrap();
287 assert_eq!(
288 events,
289 vec![StewardListEvent::ListInstalled {
290 epoch: 0,
291 retry_round: 0,
292 len: 3,
293 }]
294 );
295 assert_eq!(p.current_list().unwrap().len(), 3);
296 assert_eq!(p.election_epoch(), Some(0));
297 }
298
299 #[test]
302 fn epoch_steward_filters_by_eligibility() {
303 let mut p = DeterministicStewardList::empty(b"g".to_vec(), config());
304 let mems = members(&[1, 2, 3]);
305 let _ = p.install_list(0, &mems, 3, 0).unwrap();
306
307 let nominal = p.epoch_steward(0, |_: &[u8]| true).unwrap().to_vec();
308 let next = p
309 .epoch_steward(0, |c: &[u8]| c != nominal.as_slice())
310 .unwrap();
311 assert_ne!(next, nominal.as_slice());
312 assert!(mems.iter().any(|m| m == next));
313 }
314
315 #[test]
316 fn epoch_and_backup_distinct_when_two_eligible() {
317 let mut p =
318 DeterministicStewardList::empty(b"g".to_vec(), StewardListConfig::new(3, 3).unwrap());
319 let mems = members(&[1, 2, 3]);
320 let _ = p.install_list(0, &mems, 3, 0).unwrap();
321
322 let (e, b) = p.epoch_and_backup(0, |_: &[u8]| true);
323 assert!(e.is_some() && b.is_some());
324 assert_ne!(e.unwrap(), b.unwrap());
325 }
326
327 #[test]
330 fn backup_is_none_when_only_one_eligible() {
331 let mut p = DeterministicStewardList::empty(b"g".to_vec(), config());
332 let mems = members(&[1, 2, 3]);
333 let _ = p.install_list(0, &mems, 3, 0).unwrap();
334
335 let survivor = mems[0].clone();
336 let (e, b) = p.epoch_and_backup(0, |c: &[u8]| c == survivor.as_slice());
337 assert_eq!(e.unwrap(), survivor.as_slice());
338 assert!(b.is_none());
339 }
340
341 #[test]
344 fn bump_retry_emits_exhausted_after_max() {
345 let mut p = DeterministicStewardList::empty(b"g".to_vec(), config());
346 assert!(p.bump_retry().is_empty());
347 assert_eq!(p.retry_round(), 1);
348
349 let events = p.bump_retry();
350 assert_eq!(
351 events,
352 vec![StewardListEvent::RetryExhausted { round: 2, max: 1 }]
353 );
354 assert_eq!(p.retry_round(), 2);
355 }
356
357 #[test]
358 fn reset_retry_clears_round() {
359 let mut p = DeterministicStewardList::empty(b"g".to_vec(), config());
360 let _ = p.bump_retry();
361 let _ = p.bump_retry();
362 assert_eq!(p.retry_round(), 2);
363 p.reset_retry();
364 assert_eq!(p.retry_round(), 0);
365 }
366
367 #[test]
368 fn validate_proposed_against_self_derived_list() {
369 let mut p = DeterministicStewardList::empty(b"g".to_vec(), config());
370 let mems = members(&[1, 2, 3]);
371 let _ = p.install_list(0, &mems, 3, 0).unwrap();
372 let proposed = p.current_list().unwrap().members().to_vec();
373 assert!(p.validate_proposed(&proposed, 0, &mems, 0).unwrap());
374 }
375
376 #[test]
377 fn validate_proposed_rejects_tampered_order() {
378 let mut p = DeterministicStewardList::empty(b"g".to_vec(), config());
379 let mems = members(&[1, 2, 3]);
380 let _ = p.install_list(0, &mems, 3, 0).unwrap();
381 let mut tampered = p.current_list().unwrap().members().to_vec();
382 tampered.swap(0, 1);
383 assert!(!p.validate_proposed(&tampered, 0, &mems, 0).unwrap());
384 }
385
386 #[test]
387 fn steward_members_returns_filtered_roster() {
388 let mut p = DeterministicStewardList::empty(b"g".to_vec(), config());
389 let mems = members(&[1, 2, 3]);
390 let _ = p.install_list(0, &mems, 3, 0).unwrap();
391 let dropped = mems[0].clone();
392 let filtered = p.steward_members(|c: &[u8]| c != dropped.as_slice());
393 assert_eq!(filtered.len(), 2);
394 assert!(filtered.iter().all(|m| m != &dropped));
395 }
396
397 #[test]
398 fn set_max_retries_updates_threshold() {
399 let mut p = DeterministicStewardList::empty(b"g".to_vec(), config());
400 p.set_max_retries(3);
401 assert_eq!(p.max_retries(), 3);
402
403 for _ in 0..3 {
404 assert!(p.bump_retry().is_empty());
405 }
406 let events = p.bump_retry();
407 assert_eq!(
408 events,
409 vec![StewardListEvent::RetryExhausted { round: 4, max: 3 }]
410 );
411 }
412
413 #[test]
414 fn election_proposer_walks_eligibility() {
415 let mut p =
416 DeterministicStewardList::empty(b"g".to_vec(), StewardListConfig::new(3, 3).unwrap());
417 let mems = members(&[1, 2, 3]);
418 let _ = p.install_list(0, &mems, 3, 0).unwrap();
419
420 let proposer = p.election_proposer(|_: &[u8]| true).unwrap().to_vec();
421 let next = p
422 .election_proposer(|c: &[u8]| c != proposer.as_slice())
423 .unwrap();
424 assert_ne!(next, proposer.as_slice());
425 }
426
427 #[test]
432 fn maybe_auto_fill_installs_full_member_set_when_below_sn_min() {
433 let cfg = StewardListConfig::new(3, 5).unwrap();
434 let mut p = DeterministicStewardList::empty(b"g".to_vec(), cfg);
435 let mems = members(&[1, 2]); let events = p.maybe_auto_fill(5, &mems).unwrap();
438 assert_eq!(
439 events,
440 vec![StewardListEvent::ListInstalled {
441 epoch: 5,
442 retry_round: 0,
443 len: 2,
444 }]
445 );
446
447 let list = p.current_list().expect("auto-fill installed a list");
448 assert_eq!(list.len(), 2);
449 assert_eq!(list.retry_round(), 0);
450 for m in &mems {
451 assert!(list.contains(m), "auto-filled list must cover every member");
452 }
453 }
454
455 #[test]
458 fn maybe_auto_fill_no_op_when_at_or_above_sn_min() {
459 let cfg = StewardListConfig::new(2, 5).unwrap();
460 let mut p = DeterministicStewardList::empty(b"g".to_vec(), cfg);
461 let mems = members(&[1, 2, 3]); let events = p.maybe_auto_fill(0, &mems).unwrap();
464 assert!(events.is_empty(), "no-op above sn_min");
465 assert!(p.current_list().is_none(), "no list installed");
466 }
467}