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