Skip to main content

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}