Skip to main content

liminal_protocol/lifecycle/
obligation_dispatch.rs

1//! Move-coupled closure-debt authority and synchronous delivery seam.
2
3use crate::wire::{AckCommitted, BindingEpoch, DeliverySeq, ParticipantId};
4
5use super::{
6    ClosureState, CursorEpisodeBuildError, FrontierBinding, LiveFrontierOwner, LiveMember,
7    NonzeroDebtCursorEpisode, NonzeroParticipantAckCommit, NonzeroParticipantAckCommitError,
8};
9
10/// Failure to couple a validated live frontier to its complete debt episode.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum ObligationDebtOwnerError {
13    /// Validated frontier/observer inputs could not form one exact episode.
14    Episode(CursorEpisodeBuildError),
15    /// A nonzero commit did not apply to the exact coupled member/episode pair.
16    NonzeroAck(NonzeroParticipantAckCommitError),
17    /// A nonzero commit was attempted without one coherent Owed owner.
18    NonzeroAckState,
19}
20
21/// Complete protocol-owned authority while closure debt is nonzero.
22#[derive(Debug, PartialEq, Eq)]
23pub struct CoupledObligationDebtOwner {
24    frontier: LiveFrontierOwner,
25    episode: NonzeroDebtCursorEpisode,
26}
27
28/// Sole protocol-owned live frontier and obligation-debt dispatch state.
29#[derive(Debug, PartialEq, Eq)]
30pub enum ObligationDebtDispatchState {
31    /// Closure accounting is clear and no cursor episode exists.
32    Clear(LiveFrontierOwner),
33    /// Closure accounting is owed and one complete episode is move-coupled to it.
34    Owed(CoupledObligationDebtOwner),
35}
36
37/// Move-only authority that prevents production from reinstalling a bare
38/// frontier after beginning a coupled transition.
39#[derive(Debug)]
40pub struct ObligationDebtDispatchTransition {
41    prior_episode: Option<NonzeroDebtCursorEpisode>,
42}
43
44impl ObligationDebtDispatchState {
45    /// Couples one validated frontier according to its resulting closure state.
46    ///
47    /// # Errors
48    ///
49    /// Returns [`ObligationDebtOwnerError`] if an Owed frontier cannot form a
50    /// complete episode from the same validated frontier and observer state.
51    pub fn from_frontier(
52        frontier: LiveFrontierOwner,
53        observer_progress: DeliverySeq,
54    ) -> Result<Self, ObligationDebtOwnerError> {
55        match frontier.closure_accounting().state() {
56            ClosureState::Clear => Ok(Self::Clear(frontier)),
57            ClosureState::Owed { debt, .. } => {
58                let episode = NonzeroDebtCursorEpisode::from_claim_frontiers(
59                    frontier.frontiers(),
60                    debt,
61                    observer_progress,
62                )
63                .map_err(ObligationDebtOwnerError::Episode)?;
64                Ok(Self::Owed(CoupledObligationDebtOwner { frontier, episode }))
65            }
66        }
67    }
68
69    /// Borrows the one live frontier owner.
70    #[must_use]
71    pub const fn frontier(&self) -> &LiveFrontierOwner {
72        match self {
73            Self::Clear(frontier) => frontier,
74            Self::Owed(coupled) => &coupled.frontier,
75        }
76    }
77
78    /// Borrows the episode only on the Owed branch.
79    #[must_use]
80    pub const fn episode(&self) -> Option<&NonzeroDebtCursorEpisode> {
81        match self {
82            Self::Clear(_) => None,
83            Self::Owed(coupled) => Some(&coupled.episode),
84        }
85    }
86
87    /// Returns one participant's binding-tagged protocol cursor from the live
88    /// frontier on both owner variants.
89    #[must_use]
90    pub fn frontier_participant(
91        &self,
92        participant_id: ParticipantId,
93    ) -> Option<(FrontierBinding, DeliverySeq)> {
94        self.frontier()
95            .frontiers()
96            .active_identities()
97            .participants()
98            .iter()
99            .find(|participant| participant.participant_index() == participant_id)
100            .map(|participant| (participant.binding(), participant.cursor()))
101    }
102
103    /// Returns one participant's binding-tagged episode cursor on Owed.
104    #[must_use]
105    pub fn participant(
106        &self,
107        participant_id: ParticipantId,
108    ) -> Option<(FrontierBinding, DeliverySeq)> {
109        self.episode()?.participant_binding(participant_id)
110    }
111
112    /// Begins one consuming frontier transition while retaining the prior
113    /// episode in an unforgeable completion token.
114    #[must_use]
115    pub fn begin_transition(self) -> (LiveFrontierOwner, ObligationDebtDispatchTransition) {
116        match self {
117            Self::Clear(frontier) => (
118                frontier,
119                ObligationDebtDispatchTransition {
120                    prior_episode: None,
121                },
122            ),
123            Self::Owed(coupled) => (
124                coupled.frontier,
125                ObligationDebtDispatchTransition {
126                    prior_episode: Some(coupled.episode),
127                },
128            ),
129        }
130    }
131}
132
133impl ObligationDebtDispatchTransition {
134    /// Completes a coupled transition from the operation's exact resulting
135    /// frontier. Clear consumes any old episode; Owed reconciles all tagged
136    /// participants and surviving facts into one complete episode.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`ObligationDebtOwnerError`] if resulting frontier and observer
141    /// state cannot form one coherent owner.
142    pub fn complete(
143        self,
144        frontier: LiveFrontierOwner,
145        observer_progress: DeliverySeq,
146    ) -> Result<ObligationDebtDispatchState, ObligationDebtOwnerError> {
147        match frontier.closure_accounting().state() {
148            ClosureState::Clear => Ok(ObligationDebtDispatchState::Clear(frontier)),
149            ClosureState::Owed { debt, .. } => {
150                let episode = self
151                    .prior_episode
152                    .map_or_else(
153                        || {
154                            NonzeroDebtCursorEpisode::from_claim_frontiers(
155                                frontier.frontiers(),
156                                debt,
157                                observer_progress,
158                            )
159                        },
160                        |episode| {
161                            episode.reconcile_claim_frontiers(
162                                frontier.frontiers(),
163                                debt,
164                                observer_progress,
165                            )
166                        },
167                    )
168                    .map_err(ObligationDebtOwnerError::Episode)?;
169                Ok(ObligationDebtDispatchState::Owed(
170                    CoupledObligationDebtOwner { frontier, episode },
171                ))
172            }
173        }
174    }
175
176    /// Completes one nonzero acknowledgement with the exact episode produced by
177    /// its sealed commit instead of regenerating cursor facts from frontier
178    /// scalars. The member, episode, and frontier therefore cross one protocol
179    /// barrier as the same commit.
180    ///
181    /// # Errors
182    ///
183    /// Returns [`ObligationDebtOwnerError`] if the transition did not begin from
184    /// Owed, the aggregate commit rejects the member/episode pair, or the exact
185    /// resulting episode disagrees with the resulting frontier and closure debt.
186    pub fn complete_nonzero_ack<F>(
187        mut self,
188        frontier: LiveFrontierOwner,
189        commit: NonzeroParticipantAckCommit,
190        member: &mut LiveMember<F>,
191        observer_progress: DeliverySeq,
192    ) -> Result<(ObligationDebtDispatchState, AckCommitted), ObligationDebtOwnerError> {
193        let ClosureState::Owed { debt, .. } = frontier.closure_accounting().state() else {
194            return Err(ObligationDebtOwnerError::NonzeroAckState);
195        };
196        let mut episode = self
197            .prior_episode
198            .take()
199            .ok_or(ObligationDebtOwnerError::NonzeroAckState)?;
200        let outcome = commit
201            .apply_to(member, &mut episode)
202            .map_err(ObligationDebtOwnerError::NonzeroAck)?;
203        if episode.debt() != debt {
204            return Err(ObligationDebtOwnerError::NonzeroAckState);
205        }
206        let reconciled = episode
207            .clone()
208            .reconcile_claim_frontiers(frontier.frontiers(), debt, observer_progress)
209            .map_err(ObligationDebtOwnerError::Episode)?;
210        if reconciled != episode {
211            return Err(ObligationDebtOwnerError::NonzeroAckState);
212        }
213        Ok((
214            ObligationDebtDispatchState::Owed(CoupledObligationDebtOwner { frontier, episode }),
215            outcome,
216        ))
217    }
218}
219
220/// Internal scheduling deferral selected by the completed Leg 2 decision body.
221#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222pub enum DebtDispatchDeferral {
223    /// No durable recipient obligation follows the reconciled cursor.
224    NoObligation,
225    /// The participant has no exact current binding.
226    NoCurrentBinding,
227    /// The least testified endpoint is beyond the active debt high watermark.
228    BeyondDebtHighWatermark,
229}
230
231/// Typed disagreement that must fail dispatch rather than fabricate a wire value.
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum DebtDispatchInvariant {
234    /// Coupled closure and episode authority disagree.
235    CoupledState,
236    /// Binding or cursor authority differs across the locked snapshot.
237    ParticipantAuthority,
238    /// Outbox selection did not advance beyond the reconciled cursor.
239    OutboxSelection,
240    /// The two nonzero acknowledgement selectors diverged.
241    ScalarDivergence,
242}
243
244/// Total synchronous result at the participant delivery-decision seam.
245#[derive(Debug, PartialEq, Eq)]
246pub enum ObligationDebtDispatchDecision<T> {
247    /// The selected durable obligation may proceed to publication construction.
248    Permit(T),
249    /// Current debt authority does not permit work yet.
250    Defer(DebtDispatchDeferral),
251    /// Locked authority is internally inconsistent.
252    Invariant(DebtDispatchInvariant),
253}
254
255/// Executes the protocol-owned dispatch decision under the conversation owner.
256///
257/// The caller supplies the already-reconciled cursor and a one-shot read of the
258/// least durable recipient obligation strictly after it. Clear authority permits
259/// any advancing selection. Owed authority additionally requires exact agreement
260/// between frontier and episode participant state, then treats endpoint testimony
261/// and the episode high watermark as the complete eligibility domain: the
262/// physical retention floor is deliberately not consulted.
263pub fn decide_obligation_debt_dispatch<T>(
264    state: &ObligationDebtDispatchState,
265    participant_id: ParticipantId,
266    binding_epoch: BindingEpoch,
267    dispatch_after: DeliverySeq,
268    select_next: impl FnOnce(ParticipantId, BindingEpoch, DeliverySeq) -> Option<(DeliverySeq, T)>,
269) -> ObligationDebtDispatchDecision<T> {
270    let frontier_participant = state
271        .frontier()
272        .frontiers()
273        .active_identities()
274        .participants()
275        .iter()
276        .find(|participant| participant.participant_index() == participant_id)
277        .copied();
278    let Some(frontier_participant) = frontier_participant else {
279        return ObligationDebtDispatchDecision::Invariant(
280            DebtDispatchInvariant::ParticipantAuthority,
281        );
282    };
283    let FrontierBinding::Bound(frontier_epoch) = frontier_participant.binding() else {
284        return ObligationDebtDispatchDecision::Defer(DebtDispatchDeferral::NoCurrentBinding);
285    };
286    if frontier_epoch != binding_epoch || frontier_participant.cursor() > dispatch_after {
287        return ObligationDebtDispatchDecision::Invariant(
288            DebtDispatchInvariant::ParticipantAuthority,
289        );
290    }
291
292    let owed_high_watermark = match state {
293        ObligationDebtDispatchState::Clear(frontier) => {
294            if !matches!(frontier.closure_accounting().state(), ClosureState::Clear) {
295                return ObligationDebtDispatchDecision::Invariant(
296                    DebtDispatchInvariant::CoupledState,
297                );
298            }
299            None
300        }
301        ObligationDebtDispatchState::Owed(coupled) => {
302            let ClosureState::Owed { debt, .. } = coupled.frontier.closure_accounting().state()
303            else {
304                return ObligationDebtDispatchDecision::Invariant(
305                    DebtDispatchInvariant::CoupledState,
306                );
307            };
308            if coupled.episode.debt() != debt {
309                return ObligationDebtDispatchDecision::Invariant(
310                    DebtDispatchInvariant::CoupledState,
311                );
312            }
313            if coupled.episode.participant_binding(participant_id)
314                != Some((
315                    FrontierBinding::Bound(binding_epoch),
316                    frontier_participant.cursor(),
317                ))
318            {
319                return ObligationDebtDispatchDecision::Invariant(
320                    DebtDispatchInvariant::ParticipantAuthority,
321                );
322            }
323            Some(coupled.episode.candidate_high_watermark())
324        }
325    };
326
327    classify_selected_obligation(
328        dispatch_after,
329        owed_high_watermark,
330        select_next(participant_id, binding_epoch, dispatch_after),
331    )
332}
333
334fn classify_selected_obligation<T>(
335    dispatch_after: DeliverySeq,
336    owed_high_watermark: Option<DeliverySeq>,
337    selected: Option<(DeliverySeq, T)>,
338) -> ObligationDebtDispatchDecision<T> {
339    let Some((delivery_seq, selected)) = selected else {
340        return ObligationDebtDispatchDecision::Defer(DebtDispatchDeferral::NoObligation);
341    };
342    if delivery_seq <= dispatch_after {
343        return ObligationDebtDispatchDecision::Invariant(DebtDispatchInvariant::OutboxSelection);
344    }
345    if owed_high_watermark.is_some_and(|high_watermark| delivery_seq > high_watermark) {
346        return ObligationDebtDispatchDecision::Defer(
347            DebtDispatchDeferral::BeyondDebtHighWatermark,
348        );
349    }
350    ObligationDebtDispatchDecision::Permit(selected)
351}
352
353#[cfg(test)]
354mod tests {
355    use alloc::vec;
356
357    use crate::{
358        algebra::WideResourceVector,
359        wire::{BindingEpoch, ConnectionIncarnation, Generation},
360    };
361
362    use super::{
363        super::{BoundParticipantCursor, ClosureDebt, NonzeroDebtCursorEpisode},
364        DebtDispatchDeferral, DebtDispatchInvariant, ObligationDebtDispatchDecision,
365        classify_selected_obligation,
366    };
367
368    fn binding_epoch() -> BindingEpoch {
369        BindingEpoch::new(
370            ConnectionIncarnation::new(1, 1),
371            Generation::new(1).unwrap_or(Generation::ONE),
372        )
373    }
374
375    #[test]
376    fn nonzero_debt_permits_testified_below_floor_and_defers_above_high_watermark() {
377        let cursor = 0;
378        let high_watermark = 100;
379        let physical_floor = 25;
380        let capacity_floor = 25;
381        let below_floor_endpoint = 10;
382        let above_high_watermark = high_watermark + 1;
383        let episode = NonzeroDebtCursorEpisode::new(
384            1,
385            ClosureDebt::new(WideResourceVector::new(1, 1))
386                .unwrap_or_else(|| unreachable!("fixture debt is nonzero")),
387            high_watermark,
388            high_watermark,
389            physical_floor,
390            capacity_floor,
391            vec![BoundParticipantCursor::new(0, binding_epoch(), cursor)],
392        )
393        .unwrap_or_else(|error| unreachable!("fixture episode must be valid: {error:?}"));
394
395        assert_eq!(episode.candidate_high_watermark(), high_watermark);
396        assert_eq!(episode.observer_progress(), high_watermark);
397        assert_eq!(episode.cap_floor(), capacity_floor);
398        assert_eq!(episode.retained_suffix_start(), Some(25));
399        assert!(!episode.retains(below_floor_endpoint));
400        assert_eq!(
401            classify_selected_obligation(
402                cursor,
403                Some(episode.candidate_high_watermark()),
404                Some((below_floor_endpoint, below_floor_endpoint)),
405            ),
406            ObligationDebtDispatchDecision::Permit(below_floor_endpoint)
407        );
408        assert_eq!(
409            classify_selected_obligation(
410                cursor,
411                Some(episode.candidate_high_watermark()),
412                Some((above_high_watermark, above_high_watermark)),
413            ),
414            ObligationDebtDispatchDecision::Defer(DebtDispatchDeferral::BeyondDebtHighWatermark)
415        );
416        assert_eq!(
417            classify_selected_obligation::<u64>(
418                cursor,
419                Some(episode.candidate_high_watermark()),
420                None,
421            ),
422            ObligationDebtDispatchDecision::Defer(DebtDispatchDeferral::NoObligation)
423        );
424    }
425
426    #[test]
427    fn debt_dispatch_invariant_never_falls_back_or_fabricates_wire_refusal() {
428        let cursor = 7;
429        let selected_payload = "durable publication";
430        let decision =
431            classify_selected_obligation(cursor, Some(100), Some((cursor, selected_payload)));
432
433        assert_eq!(
434            decision,
435            ObligationDebtDispatchDecision::Invariant(DebtDispatchInvariant::OutboxSelection)
436        );
437        assert!(!matches!(
438            decision,
439            ObligationDebtDispatchDecision::Permit(_)
440        ));
441        assert!(!matches!(
442            decision,
443            ObligationDebtDispatchDecision::Defer(_)
444        ));
445    }
446}