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::sourced(
353            self.terminal.conversation_id(),
354            self.terminal.delivery_seq(),
355            super::checkpoint_codec::ObserverProjectionSource::Terminal(
356                super::CommittedBindingTerminal::Detached(self.terminal).storage_description(),
357            ),
358        )
359    }
360
361    /// Consumes the atomic result into its persistence and response values.
362    #[must_use]
363    #[allow(clippy::type_complexity)]
364    pub fn into_parts(
365        self,
366    ) -> (
367        LiveMember<EF>,
368        CommittedDetachedTerminal,
369        BindingState,
370        CommittedDetach<V>,
371        DetachCommitted,
372    ) {
373        (
374            self.member,
375            self.terminal,
376            self.binding_state,
377            self.cell,
378            self.outcome,
379        )
380    }
381}
382
383/// Atomic durable result of accepting an observer-blocked detach.
384#[derive(Clone, Debug, PartialEq, Eq)]
385pub struct PendingDetachTransition<EF, V> {
386    member: LiveMember<EF>,
387    binding_state: BindingState,
388    cell: PendingDetach<V>,
389    outcome: ObserverBackpressure,
390}
391
392impl<EF, V> PendingDetachTransition<EF, V> {
393    /// Borrows live membership retained unchanged until the terminal drains.
394    #[must_use]
395    pub const fn member(&self) -> &LiveMember<EF> {
396        &self.member
397    }
398
399    /// Returns the cause-partitioned pending-finalization binding slot.
400    #[must_use]
401    pub const fn binding_state(&self) -> BindingState {
402        self.binding_state
403    }
404
405    /// Borrows the exact token-bearing pending detach replay cell.
406    #[must_use]
407    pub const fn cell(&self) -> &PendingDetach<V> {
408        &self.cell
409    }
410
411    /// Borrows the exact initial observer-backpressure response.
412    #[must_use]
413    pub const fn outcome(&self) -> &ObserverBackpressure {
414        &self.outcome
415    }
416
417    /// Consumes the atomic result into its persistence and response values.
418    #[must_use]
419    pub fn into_parts(
420        self,
421    ) -> (
422        LiveMember<EF>,
423        BindingState,
424        PendingDetach<V>,
425        ObserverBackpressure,
426    ) {
427        (self.member, self.binding_state, self.cell, self.outcome)
428    }
429}
430
431/// Atomic durable result of exact-token pending replay.
432#[derive(Clone, Debug, PartialEq, Eq)]
433pub enum PendingReplay<EF, V> {
434    /// Candidate remains pending, either unchanged or rewritten to a newer epoch.
435    Pending {
436        /// Live membership retained unchanged while the terminal remains pending.
437        member: LiveMember<EF>,
438        /// Pending-finalization binding state retained atomically.
439        binding_state: BindingState,
440        /// Complete replacement pending cell.
441        cell: PendingDetach<V>,
442        /// Observer refusal carrying the cell's current epoch.
443        outcome: ObserverBackpressure,
444    },
445    /// Candidate drained and the cell committed with its real sequence.
446    Committed {
447        /// Live membership with the exact committed terminal persisted.
448        member: LiveMember<EF>,
449        /// Cause-partitioned terminal committed by the ordered drain.
450        terminal: CommittedDetachedTerminal,
451        /// Detached binding state after terminal append.
452        binding_state: BindingState,
453        /// Stable committed replay cell.
454        cell: CommittedDetach<V>,
455        /// Stable committed detach response.
456        outcome: DetachCommitted,
457    },
458}
459
460/// Detach request proven to match one active binding.
461#[derive(Clone, Debug)]
462pub struct VerifiedDetachRequest<V> {
463    binding: ActiveBinding,
464    request: DetachRequest,
465    request_verifier: V,
466}
467
468impl ActiveBinding {
469    /// Verifies a detach request against this exact binding epoch.
470    ///
471    /// # Errors
472    ///
473    /// Returns [`DetachVerificationError`] for the first mismatching authority
474    /// component. The caller supplies the canonical request verifier computed
475    /// by the consuming server's cryptographic layer.
476    pub fn verify_detach_request<V>(
477        &self,
478        request: DetachRequest,
479        request_verifier: V,
480    ) -> Result<VerifiedDetachRequest<V>, DetachVerificationError> {
481        if request.conversation_id != self.conversation_id {
482            return Err(DetachVerificationError::Conversation);
483        }
484        if request.participant_id != self.participant_id {
485            return Err(DetachVerificationError::Participant);
486        }
487        if request.capability_generation != self.binding_epoch.capability_generation {
488            return Err(DetachVerificationError::Generation);
489        }
490        Ok(VerifiedDetachRequest {
491            binding: *self,
492            request,
493            request_verifier,
494        })
495    }
496}
497
498/// Commits an immediate detach atomically with binding release.
499///
500/// # Errors
501///
502/// Returns [`DetachCommitError::PendingCell`] when an accepted pending detach
503/// already occupies the identity slot.
504pub fn commit_detach<EF, V: Copy + Eq>(
505    member: LiveMember<EF>,
506    verified: VerifiedDetachRequest<V>,
507    previous_cell: DetachCell<V>,
508    position: CommittedBindingTerminalPosition,
509) -> Result<CommittedDetachTransition<EF, V>, DetachCommitError> {
510    validate_previous_cell(&member, &previous_cell)?;
511    let VerifiedDetachRequest {
512        binding,
513        request,
514        request_verifier,
515    } = verified;
516    validate_member_authority(&member, binding)
517        .map_err(|_| DetachCommitError::MembershipAuthority)?;
518    let terminal = binding.commit_clean_deregister(position);
519    let member = member
520        .with_committed_terminal(CommittedBindingTerminal::Detached(terminal))
521        .map_err(DetachCommitError::TerminalHistory)?;
522    let cell = CommittedDetach {
523        token: request.detach_attempt_token,
524        participant_id: request.participant_id,
525        request_generation: request.capability_generation,
526        request_verifier,
527        committed_binding_epoch: binding.binding_epoch,
528        detached_delivery_seq: position.delivery_seq(),
529    };
530    let outcome = cell.outcome(request.conversation_id);
531    Ok(CommittedDetachTransition {
532        member,
533        terminal,
534        binding_state: BindingState::Detached,
535        cell,
536        outcome,
537    })
538}
539
540/// Starts an observer-blocked detach with terminal authority already ended.
541///
542/// The stored refusal epoch is derived from `observer_progress` in this same
543/// transition. There is deliberately no independent epoch input, so an initial
544/// refusal cannot arm an epoch different from its observed progress baseline.
545///
546/// # Errors
547///
548/// Returns [`DetachCommitError::PendingCell`] when an accepted pending detach
549/// already occupies the identity slot.
550pub fn start_blocked_detach<EF, V: Copy + Eq>(
551    member: LiveMember<EF>,
552    verified: VerifiedDetachRequest<V>,
553    previous_cell: DetachCell<V>,
554    position: PendingBindingTerminalPosition,
555    observer_progress: DeliverySeq,
556) -> Result<PendingDetachTransition<EF, V>, DetachCommitError> {
557    validate_previous_cell(&member, &previous_cell)?;
558    let VerifiedDetachRequest {
559        binding,
560        request,
561        request_verifier,
562    } = verified;
563    validate_member_authority(&member, binding)
564        .map_err(|_| DetachCommitError::MembershipAuthority)?;
565    let pending = binding.pending_clean_deregister(position);
566    let admission_order = pending.admission_order();
567    let finalization = PendingFinalization::Detached(pending);
568    let cell = PendingDetach {
569        token: request.detach_attempt_token,
570        participant_id: request.participant_id,
571        request_generation: request.capability_generation,
572        request_verifier,
573        committed_binding_epoch: binding.binding_epoch,
574        admission_order,
575        refused_epoch: observer_progress,
576    };
577    let outcome = cell.backpressure(binding.conversation_id, observer_progress);
578    Ok(PendingDetachTransition {
579        member,
580        binding_state: BindingState::PendingFinalization(finalization),
581        cell,
582        outcome,
583    })
584}
585
586/// Completes one paired pending finalization and detach replay cell.
587///
588/// # Errors
589///
590/// Returns [`DetachReplayError::StatePair`] when the two durable states do not
591/// describe the same participant, binding epoch, and admission order.
592pub fn complete_pending_detach<EF, V: Copy + Eq>(
593    member: LiveMember<EF>,
594    binding_state: BindingState,
595    cell: PendingDetach<V>,
596    detached_delivery_seq: DeliverySeq,
597) -> Result<CommittedDetachTransition<EF, V>, DetachReplayError> {
598    let finalization = validate_pending_pair(binding_state, &cell, None)?;
599    validate_member_finalization(&member, finalization)?;
600    let PendingFinalization::Detached(pending) = finalization else {
601        return Err(DetachReplayError::StatePair);
602    };
603    let terminal = pending.commit(detached_delivery_seq);
604    let member = member
605        .with_committed_terminal(CommittedBindingTerminal::Detached(terminal))
606        .map_err(DetachReplayError::TerminalHistory)?;
607    let committed = cell.commit(detached_delivery_seq);
608    let outcome = committed.outcome(finalization.conversation_id());
609    Ok(CommittedDetachTransition {
610        member,
611        terminal,
612        binding_state: BindingState::Detached,
613        cell: committed,
614        outcome,
615    })
616}
617
618impl<V: Copy + Eq> PendingDetach<V> {
619    /// Verifies exact replay against the stored token, request fields, and verifier.
620    ///
621    /// # Errors
622    ///
623    /// Returns [`DetachReplayError`] at the first mismatch.
624    pub fn verify_exact(
625        &self,
626        request: &DetachRequest,
627        request_verifier: V,
628    ) -> Result<VerifiedPendingDetach<'_, V>, DetachReplayError> {
629        verify_stored_request(
630            self.token,
631            self.participant_id,
632            self.request_generation,
633            &self.request_verifier,
634            request,
635            &request_verifier,
636        )?;
637        Ok(VerifiedPendingDetach { state: self })
638    }
639
640    /// Produces the different-token result without exposing the stored token.
641    #[must_use]
642    pub const fn competing_attempt(
643        &self,
644        conversation_id: ConversationId,
645        presented_token: DetachAttemptToken,
646        presented_generation: Generation,
647    ) -> DetachInProgress {
648        DetachInProgress {
649            conversation_id,
650            participant_id: self.participant_id,
651            presented_token,
652            presented_generation,
653            committed_binding_epoch: self.committed_binding_epoch,
654        }
655    }
656
657    const fn backpressure(
658        &self,
659        conversation_id: ConversationId,
660        observer_progress: DeliverySeq,
661    ) -> ObserverBackpressure {
662        ObserverBackpressure::Detach {
663            request: DetachEnvelope {
664                conversation_id,
665                participant_id: self.participant_id(),
666                capability_generation: self.request_generation(),
667                detach_attempt_token: self.token,
668            },
669            committed_binding_epoch: self.committed_binding_epoch(),
670            state: ObserverBackpressureState::initial(observer_progress),
671        }
672    }
673
674    const fn commit(self, detached_delivery_seq: DeliverySeq) -> CommittedDetach<V> {
675        CommittedDetach {
676            token: self.token,
677            participant_id: self.participant_id,
678            request_generation: self.request_generation,
679            request_verifier: self.request_verifier,
680            committed_binding_epoch: self.committed_binding_epoch,
681            detached_delivery_seq,
682        }
683    }
684
685    pub(crate) const fn terminalize_after_attach(self) -> TerminalizedDetach<V> {
686        TerminalizedDetach {
687            token: self.token,
688            participant_id: self.participant_id,
689            request_generation: self.request_generation,
690            request_verifier: self.request_verifier,
691            committed_binding_epoch: self.committed_binding_epoch,
692        }
693    }
694}
695
696/// Exact verified view of a pending detach cell.
697#[derive(Clone, Copy, Debug)]
698pub struct VerifiedPendingDetach<'a, V> {
699    state: &'a PendingDetach<V>,
700}
701
702impl<V: Copy + Eq> VerifiedPendingDetach<'_, V> {
703    /// Prepares exact pending replay after request-verifier matching.
704    #[must_use]
705    pub const fn prepare_replay(
706        self,
707        conversation_id: ConversationId,
708        binding_state: BindingState,
709        observer_progress: DeliverySeq,
710    ) -> PendingReplayRequest<V> {
711        PendingReplayRequest {
712            conversation_id,
713            binding_state,
714            cell: *self.state,
715            observer_progress,
716        }
717    }
718}
719
720/// Verified command that forces pending replay through the progress/drain rule.
721#[derive(Clone, Debug, PartialEq, Eq)]
722pub struct PendingReplayRequest<V> {
723    conversation_id: ConversationId,
724    binding_state: BindingState,
725    cell: PendingDetach<V>,
726    observer_progress: DeliverySeq,
727}
728
729impl<V: Copy + Eq> PendingReplayRequest<V> {
730    /// Applies the exact equality-or-greater observer-progress transition.
731    ///
732    /// # Errors
733    ///
734    /// Returns [`PendingReplayError`] when progress regresses, the caller skips
735    /// the required drain, claims a drain at equal progress, or supplies a
736    /// mismatched binding finalization.
737    pub fn apply<EF>(
738        self,
739        member: LiveMember<EF>,
740        decision: PendingDrainDecision,
741    ) -> Result<PendingReplay<EF, V>, PendingReplayError> {
742        let finalization =
743            validate_pending_pair(self.binding_state, &self.cell, Some(self.conversation_id))
744                .map_err(|_| PendingReplayError::StatePair)?;
745        validate_member_finalization(&member, finalization)
746            .map_err(|_| PendingReplayError::MembershipAuthority)?;
747        if self.observer_progress < self.cell.refused_epoch() {
748            return Err(PendingReplayError::ObserverProgressRegression);
749        }
750        if self.observer_progress == self.cell.refused_epoch() {
751            if decision != PendingDrainDecision::NotAttempted {
752                return Err(PendingReplayError::UnexpectedDrain);
753            }
754            let outcome = self
755                .cell
756                .backpressure(self.conversation_id, self.observer_progress);
757            return Ok(PendingReplay::Pending {
758                member,
759                binding_state: self.binding_state,
760                cell: self.cell,
761                outcome,
762            });
763        }
764
765        match decision {
766            PendingDrainDecision::NotAttempted => Err(PendingReplayError::DrainRequired),
767            PendingDrainDecision::StillBlocked => {
768                let mut cell = self.cell;
769                cell.refused_epoch = self.observer_progress;
770                let outcome = cell.backpressure(self.conversation_id, self.observer_progress);
771                Ok(PendingReplay::Pending {
772                    member,
773                    binding_state: self.binding_state,
774                    cell,
775                    outcome,
776                })
777            }
778            PendingDrainDecision::Committed {
779                detached_delivery_seq,
780            } => complete_pending_detach(
781                member,
782                self.binding_state,
783                self.cell,
784                detached_delivery_seq,
785            )
786            .map(|transition| PendingReplay::Committed {
787                member: transition.member,
788                terminal: transition.terminal,
789                binding_state: transition.binding_state,
790                cell: transition.cell,
791                outcome: transition.outcome,
792            })
793            .map_err(map_pending_replay_error),
794        }
795    }
796}
797
798const fn map_pending_replay_error(error: DetachReplayError) -> PendingReplayError {
799    match error {
800        DetachReplayError::MembershipAuthority => PendingReplayError::MembershipAuthority,
801        DetachReplayError::TerminalHistory(error) => PendingReplayError::TerminalHistory(error),
802        DetachReplayError::Token
803        | DetachReplayError::Participant
804        | DetachReplayError::Generation
805        | DetachReplayError::RequestVerifier
806        | DetachReplayError::StatePair => PendingReplayError::StatePair,
807    }
808}
809
810pub(super) fn validate_pending_pair<V>(
811    binding_state: BindingState,
812    cell: &PendingDetach<V>,
813    expected_conversation_id: Option<ConversationId>,
814) -> Result<PendingFinalization, DetachReplayError> {
815    let BindingState::PendingFinalization(finalization) = binding_state else {
816        return Err(DetachReplayError::StatePair);
817    };
818    let conversation_mismatch = expected_conversation_id
819        .is_some_and(|conversation_id| finalization.conversation_id() != conversation_id);
820    let participant_mismatch = finalization.participant_id() != cell.participant_id();
821    let epoch_mismatch = finalization.binding_epoch() != cell.committed_binding_epoch();
822    let order_mismatch = finalization.admission_order() != cell.admission_order();
823    let cause_mismatch = finalization.detached_cause() != Some(DetachedCause::CleanDeregister);
824    if conversation_mismatch
825        || participant_mismatch
826        || epoch_mismatch
827        || order_mismatch
828        || cause_mismatch
829    {
830        return Err(DetachReplayError::StatePair);
831    }
832    Ok(finalization)
833}
834
835fn validate_member_authority<EF>(
836    member: &LiveMember<EF>,
837    binding: ActiveBinding,
838) -> Result<(), DetachReplayError> {
839    if member.participant_id() != binding.participant_id
840        || member.conversation_id() != binding.conversation_id
841        || member.generation() != binding.binding_epoch.capability_generation
842    {
843        return Err(DetachReplayError::MembershipAuthority);
844    }
845    Ok(())
846}
847
848fn validate_previous_cell<EF, V>(
849    member: &LiveMember<EF>,
850    cell: &DetachCell<V>,
851) -> Result<(), DetachCommitError> {
852    match cell {
853        DetachCell::Empty(_) => Ok(()),
854        DetachCell::Pending(_) => Err(DetachCommitError::PendingCell),
855        DetachCell::Committed(_) => Err(DetachCommitError::CommittedCell),
856        DetachCell::Terminalized(cell) => {
857            let participant_mismatch = cell.participant_id() != member.participant_id();
858            let generation_is_not_older = cell.request_generation() >= member.generation();
859            if participant_mismatch || generation_is_not_older {
860                Err(DetachCommitError::TerminalizedCellAuthority)
861            } else {
862                Ok(())
863            }
864        }
865    }
866}
867
868fn validate_member_finalization<EF>(
869    member: &LiveMember<EF>,
870    finalization: PendingFinalization,
871) -> Result<(), DetachReplayError> {
872    if member.participant_id() != finalization.participant_id()
873        || member.conversation_id() != finalization.conversation_id()
874        || member.generation() != finalization.binding_epoch().capability_generation
875    {
876        return Err(DetachReplayError::MembershipAuthority);
877    }
878    Ok(())
879}
880
881impl<V: Copy + Eq> CommittedDetach<V> {
882    /// Verifies exact replay against the stored token, request fields, and verifier.
883    ///
884    /// # Errors
885    ///
886    /// Returns [`DetachReplayError`] at the first mismatch.
887    pub fn verify_exact(
888        &self,
889        request: &DetachRequest,
890        request_verifier: V,
891    ) -> Result<VerifiedCommittedDetach<'_, V>, DetachReplayError> {
892        verify_stored_request(
893            self.token,
894            self.participant_id,
895            self.request_generation,
896            &self.request_verifier,
897            request,
898            &request_verifier,
899        )?;
900        Ok(VerifiedCommittedDetach { state: self })
901    }
902
903    pub(crate) const fn terminalize_after_attach(self) -> TerminalizedDetach<V> {
904        TerminalizedDetach {
905            token: self.token,
906            participant_id: self.participant_id,
907            request_generation: self.request_generation,
908            request_verifier: self.request_verifier,
909            committed_binding_epoch: self.committed_binding_epoch,
910        }
911    }
912
913    const fn outcome(self, conversation_id: ConversationId) -> DetachCommitted {
914        DetachCommitted::new(
915            conversation_id,
916            self.participant_id,
917            self.token,
918            self.committed_binding_epoch,
919            self.detached_delivery_seq,
920        )
921    }
922}
923
924/// Exact verified view of a committed detach cell.
925#[derive(Clone, Copy, Debug)]
926pub struct VerifiedCommittedDetach<'a, V> {
927    state: &'a CommittedDetach<V>,
928}
929
930impl<V: Copy + Eq> VerifiedCommittedDetach<'_, V> {
931    /// Replays the stable committed detach result.
932    #[must_use]
933    pub const fn outcome(self, conversation_id: ConversationId) -> DetachCommitted {
934        self.state.outcome(conversation_id)
935    }
936}
937
938impl<V> TerminalizedDetach<V> {
939    pub(crate) const fn token(&self) -> DetachAttemptToken {
940        self.token
941    }
942
943    pub(crate) const fn participant_id(&self) -> ParticipantId {
944        self.participant_id
945    }
946
947    pub(crate) const fn request_generation(&self) -> Generation {
948        self.request_generation
949    }
950
951    pub(crate) const fn committed_binding_epoch(&self) -> BindingEpoch {
952        self.committed_binding_epoch
953    }
954
955    /// Verifies an old exact request against the retained terminalized cell.
956    ///
957    /// # Errors
958    ///
959    /// Returns [`DetachReplayError`] at the first mismatch. Exact token without
960    /// the stored verifier is insufficient.
961    pub fn verify_exact(
962        &self,
963        request: &DetachRequest,
964        request_verifier: V,
965    ) -> Result<VerifiedTerminalizedDetach<'_, V>, DetachReplayError>
966    where
967        V: Copy + Eq,
968    {
969        verify_stored_request(
970            self.token,
971            self.participant_id,
972            self.request_generation,
973            &self.request_verifier,
974            request,
975            &request_verifier,
976        )?;
977        Ok(VerifiedTerminalizedDetach { state: self })
978    }
979}
980
981/// Exact verified view whose receiver is the sole state-derived constructor for
982/// [`TerminalizedDetachCell`].
983#[derive(Clone, Copy, Debug)]
984pub struct VerifiedTerminalizedDetach<'a, V> {
985    state: &'a TerminalizedDetach<V>,
986}
987
988impl<V> VerifiedTerminalizedDetach<'_, V> {
989    /// Constructs the terminalized old-cell authority response.
990    #[must_use]
991    pub const fn outcome(
992        self,
993        conversation_id: ConversationId,
994        current_generation: Generation,
995        binding_state: BindingStateView,
996    ) -> TerminalizedDetachCell {
997        TerminalizedDetachCell::from_terminalized_state(
998            self.state,
999            conversation_id,
1000            current_generation,
1001            binding_state,
1002        )
1003    }
1004}
1005
1006fn verify_stored_request<V: Eq>(
1007    token: DetachAttemptToken,
1008    participant_id: ParticipantId,
1009    request_generation: Generation,
1010    stored_verifier: &V,
1011    request: &DetachRequest,
1012    request_verifier: &V,
1013) -> Result<(), DetachReplayError> {
1014    if request.detach_attempt_token != token {
1015        return Err(DetachReplayError::Token);
1016    }
1017    if request.participant_id != participant_id {
1018        return Err(DetachReplayError::Participant);
1019    }
1020    if request.capability_generation != request_generation {
1021        return Err(DetachReplayError::Generation);
1022    }
1023    if request_verifier != stored_verifier {
1024        return Err(DetachReplayError::RequestVerifier);
1025    }
1026    Ok(())
1027}