de_mls/core/steward_list/plugin.rs
1//! Per-conversation steward-list plug-in trait and event vocabulary.
2
3use crate::core::error::CoreError;
4use crate::core::steward_list::list::{StewardList, StewardListConfig};
5
6/// Fallback ceiling on steward-election retries. One retry gives the
7/// responsible proposer a second shot with a different list composition;
8/// beyond that human/policy intervention is expected.
9pub const DEFAULT_MAX_RETRIES: u32 = 1;
10
11/// Outcome of [`StewardListPlugin::propose_election`]. `Skip` carries a
12/// brief reason for the log; `Proposed` carries the ready-to-submit
13/// election proposal (the plug-in's deterministic ordering of the
14/// supplied candidate pool).
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ElectionDecision {
17 Skip(&'static str),
18 Proposed {
19 proposed_stewards: Vec<Vec<u8>>,
20 election_epoch: u64,
21 retry_round: u32,
22 },
23}
24
25/// Event emitted by [`StewardListPlugin`] mutators. The coordinator
26/// drains them at known safe points and turns them into protocol
27/// actions.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum StewardListEvent {
30 /// A new list has been installed (creator bootstrap, joiner sync,
31 /// successful election, or `sn_min` auto-fill). Coordinator chains
32 /// into pending-update drain and `ConversationSync` broadcast.
33 ListInstalled {
34 epoch: u64,
35 retry_round: u32,
36 len: usize,
37 },
38 /// `bump_retry` pushed `retry_round` past `max_retries`. Coordinator
39 /// escalates to the Layer-3 `Deadlock` ECP.
40 RetryExhausted { round: u32, max: u32 },
41}
42
43/// Per-conversation steward list. Passive design — answers questions, does not
44/// call out to MLS, consensus, or other plug-ins.
45///
46/// Eligibility predicates flow in from the coordinator: every "live"
47/// position query takes a `Fn(&[u8]) -> bool` so the plug-in needn't
48/// know about MLS membership or removal queues. When all candidates are
49/// eligible, the predicate-based queries return the nominal rotation
50/// position; otherwise they walk forward to the next eligible steward.
51pub trait StewardListPlugin {
52 // ── Config & raw state ─────────────────────────────────────────
53
54 /// Steward list bounds + protocol-level flags.
55 fn config(&self) -> &StewardListConfig;
56
57 /// Replace the active config — joiner sync path adopts the
58 /// conversation-wide values. Preserves list and retry state; subsequent
59 /// `install_list` calls use the new bounds.
60 fn set_config(&mut self, config: StewardListConfig);
61
62 /// Borrow the active list. `None` for joiners pre-`ConversationSync`.
63 fn current_list(&self) -> Option<&StewardList>;
64
65 /// Epoch at which the active list was elected. `None` if no list.
66 fn election_epoch(&self) -> Option<u64>;
67
68 /// Current retry round (0 for fresh elections, bumped on each
69 /// rejected proposal within the same MLS epoch). Distinct from the
70 /// list's frozen `retry_round` historical tag.
71 fn retry_round(&self) -> u32;
72
73 /// Conversation-configured ceiling on steward-election retries. Joiners
74 /// pick this up via `ConversationSync`.
75 fn max_retries(&self) -> u32;
76 fn set_max_retries(&mut self, max: u32);
77
78 // ── State predicates ───────────────────────────────────────────
79
80 /// True iff `identity` sits in the active list.
81 fn is_steward(&self, identity: &[u8]) -> bool;
82
83 /// True iff `epoch` falls outside the list's covered window
84 /// `[election_epoch, election_epoch + len)`. A new election MUST
85 /// follow once the list is exhausted.
86 fn is_exhausted(&self, epoch: u64) -> bool;
87
88 // ── Position queries (eligibility-filtered) ────────────────────
89
90 /// Steward responsible for `epoch`, walking the rotation past any
91 /// candidate for whom `eligible` returns false. Pass `|_| true`
92 /// for the nominal position.
93 fn epoch_steward<F: Fn(&[u8]) -> bool>(&self, epoch: u64, eligible: F) -> Option<&[u8]>;
94
95 /// Live epoch steward + backup, guaranteed distinct when ≥2 are
96 /// eligible. Backup is `None` when fewer than two stewards are
97 /// eligible.
98 fn epoch_and_backup<F: Fn(&[u8]) -> bool>(
99 &self,
100 epoch: u64,
101 eligible: F,
102 ) -> (Option<&[u8]>, Option<&[u8]>);
103
104 /// Steward roster filtered by `eligible`. Used by the coordinator
105 /// to build `ConversationSync.steward_members` so joiners don't inherit
106 /// ghosts or members queued for removal.
107 fn steward_members<F: Fn(&[u8]) -> bool>(&self, eligible: F) -> Vec<Vec<u8>>;
108
109 /// Deterministic election proposer when the list exhausts. Walks
110 /// rotation from index 0; returns `None` if no steward is eligible.
111 fn election_proposer<F: Fn(&[u8]) -> bool>(&self, eligible: F) -> Option<&[u8]>;
112
113 // ── Mutators ───────────────────────────────────────────────────
114
115 /// Generate and install a steward list of size `sn` from
116 /// `candidate_pool`. `retry_round` is the seed fed into the
117 /// SHA256 sort and stored on the resulting list as its historical
118 /// tag — pass the round from the accepted election proposal, or 0
119 /// for creator bootstrap and `sn_min` auto-fills (no election).
120 fn install_list(
121 &mut self,
122 epoch: u64,
123 candidate_pool: &[Vec<u8>],
124 sn: usize,
125 retry_round: u32,
126 ) -> Result<Vec<StewardListEvent>, CoreError>;
127
128 /// Re-install the list when membership policy says it must change
129 /// (e.g. RFC rule: `members.len() < sn_min` ⇒ everyone is a steward).
130 /// Coordinator calls this after every membership-changing commit
131 /// without checking the rule itself; the plug-in decides whether
132 /// to act. Returns events only when a re-install actually fired.
133 fn maybe_auto_fill(
134 &mut self,
135 epoch: u64,
136 members: &[Vec<u8>],
137 ) -> Result<Vec<StewardListEvent>, CoreError>;
138
139 /// True iff `proposed` matches what this plug-in would generate
140 /// for the same parameters. Coordinator calls this on the joiner
141 /// path before applying an election result.
142 fn validate_proposed(
143 &self,
144 proposed: &[Vec<u8>],
145 epoch: u64,
146 candidate_pool: &[Vec<u8>],
147 retry_round: u32,
148 ) -> Result<bool, CoreError>;
149
150 /// Decide whether this node SHOULD file a steward-election
151 /// proposal and, if so, return the proposal contents. Coordinator
152 /// passes the candidate pool it built (MLS members minus
153 /// pending-removal targets minus any extra excludes), the
154 /// eligibility predicate (typically the same set as the pool),
155 /// `recovery = true` to bypass the list-exhaustion gate, and the
156 /// node's own identity. Plug-in handles authorization + ordering;
157 /// coordinator handles `has_election_in_flight` + the I/O submit.
158 fn propose_election<F: Fn(&[u8]) -> bool>(
159 &self,
160 epoch: u64,
161 candidate_pool: &[Vec<u8>],
162 self_identity: &[u8],
163 eligible: F,
164 recovery: bool,
165 ) -> Result<ElectionDecision, CoreError>;
166
167 /// Increment the retry round. Emits [`StewardListEvent::RetryExhausted`]
168 /// once the new round exceeds `max_retries`.
169 fn bump_retry(&mut self) -> Vec<StewardListEvent>;
170
171 /// Reset the retry round to 0 (called on accepted election or
172 /// successful commit).
173 fn reset_retry(&mut self);
174}