Skip to main content

liminal_protocol/lifecycle/
detach.rs

1use crate::wire::{
2    BindingEpoch, BindingStateView, ConversationId, DeliverySeq, DetachAttemptToken,
3    DetachCommitted, DetachEnvelope, DetachInProgress, DetachRequest, DetachedCause, Generation,
4    ObserverBackpressure, ObserverBackpressureState, ObserverEpoch, ParticipantId,
5    TerminalizedDetachCell,
6};
7
8use super::ObserverProgressProjection;
9use super::binding::{
10    ActiveBinding, AdmissionOrder, BindingState, CommittedBindingTerminal,
11    CommittedBindingTerminalPosition, CommittedDetachedTerminal, PendingBindingTerminalPosition,
12    PendingFinalization,
13};
14use super::membership::{LiveMember, MembershipInvariantError};
15
16/// Empty detach replay cell.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct EmptyDetach;
19
20/// Pending detach replay cell whose terminal record is observer-blocked.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct PendingDetach<V> {
23    token: DetachAttemptToken,
24    participant_id: ParticipantId,
25    request_generation: Generation,
26    request_verifier: V,
27    committed_binding_epoch: BindingEpoch,
28    admission_order: AdmissionOrder,
29    refused_epoch: ObserverEpoch,
30}
31
32/// Committed detach replay cell with its real Detached record sequence.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct CommittedDetach<V> {
35    token: DetachAttemptToken,
36    participant_id: ParticipantId,
37    request_generation: Generation,
38    request_verifier: V,
39    committed_binding_epoch: BindingEpoch,
40    detached_delivery_seq: DeliverySeq,
41}
42
43/// Terminalized detach replay cell retained after a successful attach.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct TerminalizedDetach<V> {
46    token: DetachAttemptToken,
47    participant_id: ParticipantId,
48    request_generation: Generation,
49    request_verifier: V,
50    committed_binding_epoch: BindingEpoch,
51}
52
53/// Exact four-variant detach cell mandated by the extraction brief.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum DetachCell<V> {
56    /// No detach replay state.
57    Empty(EmptyDetach),
58    /// Accepted detach awaits terminal append.
59    Pending(PendingDetach<V>),
60    /// Detach committed and is replayable.
61    Committed(CommittedDetach<V>),
62    /// A later attach consumed binding authority but retained old detach data.
63    Terminalized(TerminalizedDetach<V>),
64}
65
66impl<V> Default for DetachCell<V> {
67    fn default() -> Self {
68        Self::Empty(EmptyDetach)
69    }
70}
71
72pub(super) fn restore_pending_detach<V>(
73    token: DetachAttemptToken,
74    participant_id: ParticipantId,
75    request_generation: Generation,
76    request_verifier: V,
77    committed_binding_epoch: BindingEpoch,
78    admission_order: AdmissionOrder,
79    refused_epoch: ObserverEpoch,
80) -> Option<PendingDetach<V>> {
81    if committed_binding_epoch.capability_generation != request_generation
82        || admission_order.participant_index() != participant_id
83        || !matches!(
84            admission_order.candidate_phase(),
85            crate::outcome::CandidatePhase::BindingTerminal
86        )
87    {
88        return None;
89    }
90    Some(PendingDetach {
91        token,
92        participant_id,
93        request_generation,
94        request_verifier,
95        committed_binding_epoch,
96        admission_order,
97        refused_epoch,
98    })
99}
100
101pub(super) fn restore_committed_detach<V>(
102    token: DetachAttemptToken,
103    participant_id: ParticipantId,
104    request_generation: Generation,
105    request_verifier: V,
106    committed_binding_epoch: BindingEpoch,
107    detached_delivery_seq: DeliverySeq,
108) -> Option<CommittedDetach<V>> {
109    if committed_binding_epoch.capability_generation != request_generation {
110        return None;
111    }
112    Some(CommittedDetach {
113        token,
114        participant_id,
115        request_generation,
116        request_verifier,
117        committed_binding_epoch,
118        detached_delivery_seq,
119    })
120}
121
122pub(super) fn restore_terminalized_detach<V>(
123    token: DetachAttemptToken,
124    participant_id: ParticipantId,
125    request_generation: Generation,
126    request_verifier: V,
127    committed_binding_epoch: BindingEpoch,
128) -> Option<TerminalizedDetach<V>> {
129    if committed_binding_epoch.capability_generation != request_generation {
130        return None;
131    }
132    Some(TerminalizedDetach {
133        token,
134        participant_id,
135        request_generation,
136        request_verifier,
137        committed_binding_epoch,
138    })
139}
140
141impl<V> DetachCell<V> {
142    /// Extracts the mandated fourth variant without exposing its fields.
143    #[must_use]
144    pub fn into_terminalized(self) -> Option<TerminalizedDetach<V>> {
145        match self {
146            Self::Terminalized(cell) => Some(cell),
147            Self::Empty(_) | Self::Pending(_) | Self::Committed(_) => None,
148        }
149    }
150}
151
152impl<V> PendingDetach<V> {
153    pub(crate) const fn participant_id(&self) -> ParticipantId {
154        self.participant_id
155    }
156
157    pub(crate) const fn request_generation(&self) -> Generation {
158        self.request_generation
159    }
160
161    pub(crate) const fn committed_binding_epoch(&self) -> BindingEpoch {
162        self.committed_binding_epoch
163    }
164
165    pub(crate) const fn admission_order(&self) -> AdmissionOrder {
166        self.admission_order
167    }
168
169    pub(crate) const fn refused_epoch(&self) -> ObserverEpoch {
170        self.refused_epoch
171    }
172}
173
174impl<V> CommittedDetach<V> {
175    pub(crate) const fn token(&self) -> DetachAttemptToken {
176        self.token
177    }
178
179    pub(crate) const fn participant_id(&self) -> ParticipantId {
180        self.participant_id
181    }
182
183    pub(crate) const fn request_generation(&self) -> Generation {
184        self.request_generation
185    }
186
187    pub(crate) const fn committed_binding_epoch(&self) -> BindingEpoch {
188        self.committed_binding_epoch
189    }
190
191    pub(crate) const fn detached_delivery_seq(&self) -> DeliverySeq {
192        self.detached_delivery_seq
193    }
194}
195
196/// Authority mismatch between an active binding and detach request.
197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198pub enum DetachVerificationError {
199    /// Request names another conversation.
200    Conversation,
201    /// Request names another participant.
202    Participant,
203    /// Request generation differs from the binding epoch.
204    Generation,
205}
206
207/// Exact-token replay verification failure.
208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
209pub enum DetachReplayError {
210    /// Token differs from the stored cell token.
211    Token,
212    /// Participant differs from the stored cell participant.
213    Participant,
214    /// Generation differs from the stored canonical request.
215    Generation,
216    /// Canonical non-secret verifier differs.
217    RequestVerifier,
218    /// Pending-finalization state and detach cell do not describe one commit.
219    StatePair,
220    /// Live membership does not own the binding terminal being completed.
221    MembershipAuthority,
222    /// Committed terminal history violates the live membership invariant.
223    TerminalHistory(MembershipInvariantError),
224}
225
226/// Invalid previous-cell state for accepting a new detach.
227#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228pub enum DetachCommitError {
229    /// An accepted pending detach must be replayed or drained, never replaced.
230    PendingCell,
231    /// A committed detach cannot coexist with the verified current binding.
232    CommittedCell,
233    /// Retained terminalized state belongs to another participant or is not older.
234    TerminalizedCellAuthority,
235    /// Live membership does not own the verified active binding.
236    MembershipAuthority,
237    /// Committed terminal history violates the live membership invariant.
238    TerminalHistory(MembershipInvariantError),
239}
240
241/// Result of the mandatory ordered drain attempt on advanced observer progress.
242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
243pub enum PendingDrainDecision {
244    /// Progress equals the stored refusal epoch, so no drain was attempted.
245    NotAttempted,
246    /// The ordered candidate remains blocked after the required drain attempt.
247    StillBlocked,
248    /// The ordered candidate committed at this real delivery sequence.
249    Committed {
250        /// Assigned Detached record sequence.
251        detached_delivery_seq: DeliverySeq,
252    },
253}
254
255/// Invalid pending replay input or paired state.
256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
257pub enum PendingReplayError {
258    /// Observer progress regressed below the cell's stored refusal epoch.
259    ObserverProgressRegression,
260    /// Greater progress was supplied without first attempting the ordered drain.
261    DrainRequired,
262    /// Equal progress incorrectly claimed that a drain was attempted.
263    UnexpectedDrain,
264    /// Binding finalization and detach replay cell do not describe one commit.
265    StatePair,
266    /// Live membership does not own the binding terminal being replayed.
267    MembershipAuthority,
268    /// Committed terminal history violates the live membership invariant.
269    TerminalHistory(MembershipInvariantError),
270}
271
272/// Atomic durable result of an immediately committed or drained detach.
273#[derive(Clone, Debug, PartialEq, Eq)]
274pub struct CommittedDetachTransition<EF, V> {
275    member: LiveMember<EF>,
276    terminal: CommittedDetachedTerminal,
277    binding_state: BindingState,
278    cell: CommittedDetach<V>,
279    outcome: DetachCommitted,
280}
281
282impl<EF, V> CommittedDetachTransition<EF, V> {
283    /// Borrows live membership with the exact binding terminal persisted.
284    #[must_use]
285    pub const fn member(&self) -> &LiveMember<EF> {
286        &self.member
287    }
288
289    /// Returns the exact committed `Detached(CleanDeregister)` terminal.
290    #[must_use]
291    pub const fn terminal(&self) -> CommittedDetachedTerminal {
292        self.terminal
293    }
294
295    /// Returns the detached post-transition binding slot.
296    #[must_use]
297    pub const fn binding_state(&self) -> BindingState {
298        self.binding_state
299    }
300
301    /// Borrows the stable committed detach replay cell.
302    #[must_use]
303    pub const fn cell(&self) -> &CommittedDetach<V> {
304        &self.cell
305    }
306
307    /// Borrows the exact canonical committed-detach response.
308    #[must_use]
309    pub const fn outcome(&self) -> &DetachCommitted {
310        &self.outcome
311    }
312
313    /// Projects this exact committed terminal into hard observer progress.
314    #[must_use]
315    pub const fn observer_progress_projection(&self) -> ObserverProgressProjection {
316        ObserverProgressProjection::new(
317            self.terminal.conversation_id(),
318            self.terminal.delivery_seq(),
319        )
320    }
321
322    /// Consumes the atomic result into its persistence and response values.
323    #[must_use]
324    #[allow(clippy::type_complexity)]
325    pub fn into_parts(
326        self,
327    ) -> (
328        LiveMember<EF>,
329        CommittedDetachedTerminal,
330        BindingState,
331        CommittedDetach<V>,
332        DetachCommitted,
333    ) {
334        (
335            self.member,
336            self.terminal,
337            self.binding_state,
338            self.cell,
339            self.outcome,
340        )
341    }
342}
343
344/// Atomic durable result of accepting an observer-blocked detach.
345#[derive(Clone, Debug, PartialEq, Eq)]
346pub struct PendingDetachTransition<EF, V> {
347    member: LiveMember<EF>,
348    binding_state: BindingState,
349    cell: PendingDetach<V>,
350    outcome: ObserverBackpressure,
351}
352
353impl<EF, V> PendingDetachTransition<EF, V> {
354    /// Borrows live membership retained unchanged until the terminal drains.
355    #[must_use]
356    pub const fn member(&self) -> &LiveMember<EF> {
357        &self.member
358    }
359
360    /// Returns the cause-partitioned pending-finalization binding slot.
361    #[must_use]
362    pub const fn binding_state(&self) -> BindingState {
363        self.binding_state
364    }
365
366    /// Borrows the exact token-bearing pending detach replay cell.
367    #[must_use]
368    pub const fn cell(&self) -> &PendingDetach<V> {
369        &self.cell
370    }
371
372    /// Borrows the exact initial observer-backpressure response.
373    #[must_use]
374    pub const fn outcome(&self) -> &ObserverBackpressure {
375        &self.outcome
376    }
377
378    /// Consumes the atomic result into its persistence and response values.
379    #[must_use]
380    pub fn into_parts(
381        self,
382    ) -> (
383        LiveMember<EF>,
384        BindingState,
385        PendingDetach<V>,
386        ObserverBackpressure,
387    ) {
388        (self.member, self.binding_state, self.cell, self.outcome)
389    }
390}
391
392/// Atomic durable result of exact-token pending replay.
393#[derive(Clone, Debug, PartialEq, Eq)]
394pub enum PendingReplay<EF, V> {
395    /// Candidate remains pending, either unchanged or rewritten to a newer epoch.
396    Pending {
397        /// Live membership retained unchanged while the terminal remains pending.
398        member: LiveMember<EF>,
399        /// Pending-finalization binding state retained atomically.
400        binding_state: BindingState,
401        /// Complete replacement pending cell.
402        cell: PendingDetach<V>,
403        /// Observer refusal carrying the cell's current epoch.
404        outcome: ObserverBackpressure,
405    },
406    /// Candidate drained and the cell committed with its real sequence.
407    Committed {
408        /// Live membership with the exact committed terminal persisted.
409        member: LiveMember<EF>,
410        /// Cause-partitioned terminal committed by the ordered drain.
411        terminal: CommittedDetachedTerminal,
412        /// Detached binding state after terminal append.
413        binding_state: BindingState,
414        /// Stable committed replay cell.
415        cell: CommittedDetach<V>,
416        /// Stable committed detach response.
417        outcome: DetachCommitted,
418    },
419}
420
421/// Detach request proven to match one active binding.
422#[derive(Clone, Debug)]
423pub struct VerifiedDetachRequest<V> {
424    binding: ActiveBinding,
425    request: DetachRequest,
426    request_verifier: V,
427}
428
429impl ActiveBinding {
430    /// Verifies a detach request against this exact binding epoch.
431    ///
432    /// # Errors
433    ///
434    /// Returns [`DetachVerificationError`] for the first mismatching authority
435    /// component. The caller supplies the canonical request verifier computed
436    /// by the consuming server's cryptographic layer.
437    pub fn verify_detach_request<V>(
438        &self,
439        request: DetachRequest,
440        request_verifier: V,
441    ) -> Result<VerifiedDetachRequest<V>, DetachVerificationError> {
442        if request.conversation_id != self.conversation_id {
443            return Err(DetachVerificationError::Conversation);
444        }
445        if request.participant_id != self.participant_id {
446            return Err(DetachVerificationError::Participant);
447        }
448        if request.capability_generation != self.binding_epoch.capability_generation {
449            return Err(DetachVerificationError::Generation);
450        }
451        Ok(VerifiedDetachRequest {
452            binding: *self,
453            request,
454            request_verifier,
455        })
456    }
457}
458
459/// Commits an immediate detach atomically with binding release.
460///
461/// # Errors
462///
463/// Returns [`DetachCommitError::PendingCell`] when an accepted pending detach
464/// already occupies the identity slot.
465pub fn commit_detach<EF, V: Copy + Eq>(
466    member: LiveMember<EF>,
467    verified: VerifiedDetachRequest<V>,
468    previous_cell: DetachCell<V>,
469    position: CommittedBindingTerminalPosition,
470) -> Result<CommittedDetachTransition<EF, V>, DetachCommitError> {
471    validate_previous_cell(&member, &previous_cell)?;
472    let VerifiedDetachRequest {
473        binding,
474        request,
475        request_verifier,
476    } = verified;
477    validate_member_authority(&member, binding)
478        .map_err(|_| DetachCommitError::MembershipAuthority)?;
479    let terminal = binding.commit_clean_deregister(position);
480    let member = member
481        .with_committed_terminal(CommittedBindingTerminal::Detached(terminal))
482        .map_err(DetachCommitError::TerminalHistory)?;
483    let cell = CommittedDetach {
484        token: request.detach_attempt_token,
485        participant_id: request.participant_id,
486        request_generation: request.capability_generation,
487        request_verifier,
488        committed_binding_epoch: binding.binding_epoch,
489        detached_delivery_seq: position.delivery_seq(),
490    };
491    let outcome = cell.outcome(request.conversation_id);
492    Ok(CommittedDetachTransition {
493        member,
494        terminal,
495        binding_state: BindingState::Detached,
496        cell,
497        outcome,
498    })
499}
500
501/// Starts an observer-blocked detach with terminal authority already ended.
502///
503/// The stored refusal epoch is derived from `observer_progress` in this same
504/// transition. There is deliberately no independent epoch input, so an initial
505/// refusal cannot arm an epoch different from its observed progress baseline.
506///
507/// # Errors
508///
509/// Returns [`DetachCommitError::PendingCell`] when an accepted pending detach
510/// already occupies the identity slot.
511pub fn start_blocked_detach<EF, V: Copy + Eq>(
512    member: LiveMember<EF>,
513    verified: VerifiedDetachRequest<V>,
514    previous_cell: DetachCell<V>,
515    position: PendingBindingTerminalPosition,
516    observer_progress: DeliverySeq,
517) -> Result<PendingDetachTransition<EF, V>, DetachCommitError> {
518    validate_previous_cell(&member, &previous_cell)?;
519    let VerifiedDetachRequest {
520        binding,
521        request,
522        request_verifier,
523    } = verified;
524    validate_member_authority(&member, binding)
525        .map_err(|_| DetachCommitError::MembershipAuthority)?;
526    let pending = binding.pending_clean_deregister(position);
527    let admission_order = pending.admission_order();
528    let finalization = PendingFinalization::Detached(pending);
529    let cell = PendingDetach {
530        token: request.detach_attempt_token,
531        participant_id: request.participant_id,
532        request_generation: request.capability_generation,
533        request_verifier,
534        committed_binding_epoch: binding.binding_epoch,
535        admission_order,
536        refused_epoch: observer_progress,
537    };
538    let outcome = cell.backpressure(binding.conversation_id, observer_progress);
539    Ok(PendingDetachTransition {
540        member,
541        binding_state: BindingState::PendingFinalization(finalization),
542        cell,
543        outcome,
544    })
545}
546
547/// Completes one paired pending finalization and detach replay cell.
548///
549/// # Errors
550///
551/// Returns [`DetachReplayError::StatePair`] when the two durable states do not
552/// describe the same participant, binding epoch, and admission order.
553pub fn complete_pending_detach<EF, V: Copy + Eq>(
554    member: LiveMember<EF>,
555    binding_state: BindingState,
556    cell: PendingDetach<V>,
557    detached_delivery_seq: DeliverySeq,
558) -> Result<CommittedDetachTransition<EF, V>, DetachReplayError> {
559    let finalization = validate_pending_pair(binding_state, &cell, None)?;
560    validate_member_finalization(&member, finalization)?;
561    let PendingFinalization::Detached(pending) = finalization else {
562        return Err(DetachReplayError::StatePair);
563    };
564    let terminal = pending.commit(detached_delivery_seq);
565    let member = member
566        .with_committed_terminal(CommittedBindingTerminal::Detached(terminal))
567        .map_err(DetachReplayError::TerminalHistory)?;
568    let committed = cell.commit(detached_delivery_seq);
569    let outcome = committed.outcome(finalization.conversation_id());
570    Ok(CommittedDetachTransition {
571        member,
572        terminal,
573        binding_state: BindingState::Detached,
574        cell: committed,
575        outcome,
576    })
577}
578
579impl<V: Copy + Eq> PendingDetach<V> {
580    /// Verifies exact replay against the stored token, request fields, and verifier.
581    ///
582    /// # Errors
583    ///
584    /// Returns [`DetachReplayError`] at the first mismatch.
585    pub fn verify_exact(
586        &self,
587        request: &DetachRequest,
588        request_verifier: V,
589    ) -> Result<VerifiedPendingDetach<'_, V>, DetachReplayError> {
590        verify_stored_request(
591            self.token,
592            self.participant_id,
593            self.request_generation,
594            &self.request_verifier,
595            request,
596            &request_verifier,
597        )?;
598        Ok(VerifiedPendingDetach { state: self })
599    }
600
601    /// Produces the different-token result without exposing the stored token.
602    #[must_use]
603    pub const fn competing_attempt(
604        &self,
605        conversation_id: ConversationId,
606        presented_token: DetachAttemptToken,
607        presented_generation: Generation,
608    ) -> DetachInProgress {
609        DetachInProgress {
610            conversation_id,
611            participant_id: self.participant_id,
612            presented_token,
613            presented_generation,
614            committed_binding_epoch: self.committed_binding_epoch,
615        }
616    }
617
618    const fn backpressure(
619        &self,
620        conversation_id: ConversationId,
621        observer_progress: DeliverySeq,
622    ) -> ObserverBackpressure {
623        ObserverBackpressure::Detach {
624            request: DetachEnvelope {
625                conversation_id,
626                participant_id: self.participant_id(),
627                capability_generation: self.request_generation(),
628                detach_attempt_token: self.token,
629            },
630            committed_binding_epoch: self.committed_binding_epoch(),
631            state: ObserverBackpressureState::initial(observer_progress),
632        }
633    }
634
635    const fn commit(self, detached_delivery_seq: DeliverySeq) -> CommittedDetach<V> {
636        CommittedDetach {
637            token: self.token,
638            participant_id: self.participant_id,
639            request_generation: self.request_generation,
640            request_verifier: self.request_verifier,
641            committed_binding_epoch: self.committed_binding_epoch,
642            detached_delivery_seq,
643        }
644    }
645
646    pub(crate) const fn terminalize_after_attach(self) -> TerminalizedDetach<V> {
647        TerminalizedDetach {
648            token: self.token,
649            participant_id: self.participant_id,
650            request_generation: self.request_generation,
651            request_verifier: self.request_verifier,
652            committed_binding_epoch: self.committed_binding_epoch,
653        }
654    }
655}
656
657/// Exact verified view of a pending detach cell.
658#[derive(Clone, Copy, Debug)]
659pub struct VerifiedPendingDetach<'a, V> {
660    state: &'a PendingDetach<V>,
661}
662
663impl<V: Copy + Eq> VerifiedPendingDetach<'_, V> {
664    /// Prepares exact pending replay after request-verifier matching.
665    #[must_use]
666    pub const fn prepare_replay(
667        self,
668        conversation_id: ConversationId,
669        binding_state: BindingState,
670        observer_progress: DeliverySeq,
671    ) -> PendingReplayRequest<V> {
672        PendingReplayRequest {
673            conversation_id,
674            binding_state,
675            cell: *self.state,
676            observer_progress,
677        }
678    }
679}
680
681/// Verified command that forces pending replay through the progress/drain rule.
682#[derive(Clone, Debug, PartialEq, Eq)]
683pub struct PendingReplayRequest<V> {
684    conversation_id: ConversationId,
685    binding_state: BindingState,
686    cell: PendingDetach<V>,
687    observer_progress: DeliverySeq,
688}
689
690impl<V: Copy + Eq> PendingReplayRequest<V> {
691    /// Applies the exact equality-or-greater observer-progress transition.
692    ///
693    /// # Errors
694    ///
695    /// Returns [`PendingReplayError`] when progress regresses, the caller skips
696    /// the required drain, claims a drain at equal progress, or supplies a
697    /// mismatched binding finalization.
698    pub fn apply<EF>(
699        self,
700        member: LiveMember<EF>,
701        decision: PendingDrainDecision,
702    ) -> Result<PendingReplay<EF, V>, PendingReplayError> {
703        let finalization =
704            validate_pending_pair(self.binding_state, &self.cell, Some(self.conversation_id))
705                .map_err(|_| PendingReplayError::StatePair)?;
706        validate_member_finalization(&member, finalization)
707            .map_err(|_| PendingReplayError::MembershipAuthority)?;
708        if self.observer_progress < self.cell.refused_epoch() {
709            return Err(PendingReplayError::ObserverProgressRegression);
710        }
711        if self.observer_progress == self.cell.refused_epoch() {
712            if decision != PendingDrainDecision::NotAttempted {
713                return Err(PendingReplayError::UnexpectedDrain);
714            }
715            let outcome = self
716                .cell
717                .backpressure(self.conversation_id, self.observer_progress);
718            return Ok(PendingReplay::Pending {
719                member,
720                binding_state: self.binding_state,
721                cell: self.cell,
722                outcome,
723            });
724        }
725
726        match decision {
727            PendingDrainDecision::NotAttempted => Err(PendingReplayError::DrainRequired),
728            PendingDrainDecision::StillBlocked => {
729                let mut cell = self.cell;
730                cell.refused_epoch = self.observer_progress;
731                let outcome = cell.backpressure(self.conversation_id, self.observer_progress);
732                Ok(PendingReplay::Pending {
733                    member,
734                    binding_state: self.binding_state,
735                    cell,
736                    outcome,
737                })
738            }
739            PendingDrainDecision::Committed {
740                detached_delivery_seq,
741            } => complete_pending_detach(
742                member,
743                self.binding_state,
744                self.cell,
745                detached_delivery_seq,
746            )
747            .map(|transition| PendingReplay::Committed {
748                member: transition.member,
749                terminal: transition.terminal,
750                binding_state: transition.binding_state,
751                cell: transition.cell,
752                outcome: transition.outcome,
753            })
754            .map_err(map_pending_replay_error),
755        }
756    }
757}
758
759const fn map_pending_replay_error(error: DetachReplayError) -> PendingReplayError {
760    match error {
761        DetachReplayError::MembershipAuthority => PendingReplayError::MembershipAuthority,
762        DetachReplayError::TerminalHistory(error) => PendingReplayError::TerminalHistory(error),
763        DetachReplayError::Token
764        | DetachReplayError::Participant
765        | DetachReplayError::Generation
766        | DetachReplayError::RequestVerifier
767        | DetachReplayError::StatePair => PendingReplayError::StatePair,
768    }
769}
770
771pub(super) fn validate_pending_pair<V>(
772    binding_state: BindingState,
773    cell: &PendingDetach<V>,
774    expected_conversation_id: Option<ConversationId>,
775) -> Result<PendingFinalization, DetachReplayError> {
776    let BindingState::PendingFinalization(finalization) = binding_state else {
777        return Err(DetachReplayError::StatePair);
778    };
779    let conversation_mismatch = expected_conversation_id
780        .is_some_and(|conversation_id| finalization.conversation_id() != conversation_id);
781    let participant_mismatch = finalization.participant_id() != cell.participant_id();
782    let epoch_mismatch = finalization.binding_epoch() != cell.committed_binding_epoch();
783    let order_mismatch = finalization.admission_order() != cell.admission_order();
784    let cause_mismatch = finalization.detached_cause() != Some(DetachedCause::CleanDeregister);
785    if conversation_mismatch
786        || participant_mismatch
787        || epoch_mismatch
788        || order_mismatch
789        || cause_mismatch
790    {
791        return Err(DetachReplayError::StatePair);
792    }
793    Ok(finalization)
794}
795
796fn validate_member_authority<EF>(
797    member: &LiveMember<EF>,
798    binding: ActiveBinding,
799) -> Result<(), DetachReplayError> {
800    if member.participant_id() != binding.participant_id
801        || member.conversation_id() != binding.conversation_id
802        || member.generation() != binding.binding_epoch.capability_generation
803    {
804        return Err(DetachReplayError::MembershipAuthority);
805    }
806    Ok(())
807}
808
809fn validate_previous_cell<EF, V>(
810    member: &LiveMember<EF>,
811    cell: &DetachCell<V>,
812) -> Result<(), DetachCommitError> {
813    match cell {
814        DetachCell::Empty(_) => Ok(()),
815        DetachCell::Pending(_) => Err(DetachCommitError::PendingCell),
816        DetachCell::Committed(_) => Err(DetachCommitError::CommittedCell),
817        DetachCell::Terminalized(cell) => {
818            let participant_mismatch = cell.participant_id() != member.participant_id();
819            let generation_is_not_older = cell.request_generation() >= member.generation();
820            if participant_mismatch || generation_is_not_older {
821                Err(DetachCommitError::TerminalizedCellAuthority)
822            } else {
823                Ok(())
824            }
825        }
826    }
827}
828
829fn validate_member_finalization<EF>(
830    member: &LiveMember<EF>,
831    finalization: PendingFinalization,
832) -> Result<(), DetachReplayError> {
833    if member.participant_id() != finalization.participant_id()
834        || member.conversation_id() != finalization.conversation_id()
835        || member.generation() != finalization.binding_epoch().capability_generation
836    {
837        return Err(DetachReplayError::MembershipAuthority);
838    }
839    Ok(())
840}
841
842impl<V: Copy + Eq> CommittedDetach<V> {
843    /// Verifies exact replay against the stored token, request fields, and verifier.
844    ///
845    /// # Errors
846    ///
847    /// Returns [`DetachReplayError`] at the first mismatch.
848    pub fn verify_exact(
849        &self,
850        request: &DetachRequest,
851        request_verifier: V,
852    ) -> Result<VerifiedCommittedDetach<'_, V>, DetachReplayError> {
853        verify_stored_request(
854            self.token,
855            self.participant_id,
856            self.request_generation,
857            &self.request_verifier,
858            request,
859            &request_verifier,
860        )?;
861        Ok(VerifiedCommittedDetach { state: self })
862    }
863
864    pub(crate) const fn terminalize_after_attach(self) -> TerminalizedDetach<V> {
865        TerminalizedDetach {
866            token: self.token,
867            participant_id: self.participant_id,
868            request_generation: self.request_generation,
869            request_verifier: self.request_verifier,
870            committed_binding_epoch: self.committed_binding_epoch,
871        }
872    }
873
874    const fn outcome(self, conversation_id: ConversationId) -> DetachCommitted {
875        DetachCommitted::new(
876            conversation_id,
877            self.participant_id,
878            self.token,
879            self.committed_binding_epoch,
880            self.detached_delivery_seq,
881        )
882    }
883}
884
885/// Exact verified view of a committed detach cell.
886#[derive(Clone, Copy, Debug)]
887pub struct VerifiedCommittedDetach<'a, V> {
888    state: &'a CommittedDetach<V>,
889}
890
891impl<V: Copy + Eq> VerifiedCommittedDetach<'_, V> {
892    /// Replays the stable committed detach result.
893    #[must_use]
894    pub const fn outcome(self, conversation_id: ConversationId) -> DetachCommitted {
895        self.state.outcome(conversation_id)
896    }
897}
898
899impl<V> TerminalizedDetach<V> {
900    pub(crate) const fn token(&self) -> DetachAttemptToken {
901        self.token
902    }
903
904    pub(crate) const fn participant_id(&self) -> ParticipantId {
905        self.participant_id
906    }
907
908    pub(crate) const fn request_generation(&self) -> Generation {
909        self.request_generation
910    }
911
912    pub(crate) const fn committed_binding_epoch(&self) -> BindingEpoch {
913        self.committed_binding_epoch
914    }
915
916    /// Verifies an old exact request against the retained terminalized cell.
917    ///
918    /// # Errors
919    ///
920    /// Returns [`DetachReplayError`] at the first mismatch. Exact token without
921    /// the stored verifier is insufficient.
922    pub fn verify_exact(
923        &self,
924        request: &DetachRequest,
925        request_verifier: V,
926    ) -> Result<VerifiedTerminalizedDetach<'_, V>, DetachReplayError>
927    where
928        V: Copy + Eq,
929    {
930        verify_stored_request(
931            self.token,
932            self.participant_id,
933            self.request_generation,
934            &self.request_verifier,
935            request,
936            &request_verifier,
937        )?;
938        Ok(VerifiedTerminalizedDetach { state: self })
939    }
940}
941
942/// Exact verified view whose receiver is the sole state-derived constructor for
943/// [`TerminalizedDetachCell`].
944#[derive(Clone, Copy, Debug)]
945pub struct VerifiedTerminalizedDetach<'a, V> {
946    state: &'a TerminalizedDetach<V>,
947}
948
949impl<V> VerifiedTerminalizedDetach<'_, V> {
950    /// Constructs the terminalized old-cell authority response.
951    #[must_use]
952    pub const fn outcome(
953        self,
954        conversation_id: ConversationId,
955        current_generation: Generation,
956        binding_state: BindingStateView,
957    ) -> TerminalizedDetachCell {
958        TerminalizedDetachCell::from_terminalized_state(
959            self.state,
960            conversation_id,
961            current_generation,
962            binding_state,
963        )
964    }
965}
966
967fn verify_stored_request<V: Eq>(
968    token: DetachAttemptToken,
969    participant_id: ParticipantId,
970    request_generation: Generation,
971    stored_verifier: &V,
972    request: &DetachRequest,
973    request_verifier: &V,
974) -> Result<(), DetachReplayError> {
975    if request.detach_attempt_token != token {
976        return Err(DetachReplayError::Token);
977    }
978    if request.participant_id != participant_id {
979        return Err(DetachReplayError::Participant);
980    }
981    if request.capability_generation != request_generation {
982        return Err(DetachReplayError::Generation);
983    }
984    if request_verifier != stored_verifier {
985        return Err(DetachReplayError::RequestVerifier);
986    }
987    Ok(())
988}