Skip to main content

liminal_protocol/lifecycle/
membership.rs

1use crate::outcome::CandidatePhase;
2use crate::wire::{
3    AttachSecret, BindingEpoch, ConversationId, DeliverySeq, DetachedCause, Generation,
4    LeaveAttemptToken, LeaveCommitted, LeaveRequest, ParticipantId, TransactionOrder,
5};
6
7use super::{
8    AdmissionOrder, BindingState, ClaimFrontiers, CommittedBindingTerminal, DetachCell,
9    PendingFinalization, detach::validate_pending_pair, lookup::AttachSecretProof,
10};
11
12/// Consuming-layer enrollment-token fingerprint with no protocol-invented width.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct EnrollmentFingerprint<F>(F);
15
16impl<F> EnrollmentFingerprint<F> {
17    /// Wraps the consuming cryptographic layer's canonical fingerprint value.
18    #[must_use]
19    pub const fn new(value: F) -> Self {
20        Self(value)
21    }
22
23    /// Borrows the consuming-layer fingerprint.
24    #[must_use]
25    pub const fn value(&self) -> &F {
26        &self.0
27    }
28
29    /// Consumes the wrapper and returns the fingerprint value.
30    #[must_use]
31    pub fn into_inner(self) -> F {
32        self.0
33    }
34}
35
36/// Consuming-layer canonical Leave-request fingerprint.
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct LeaveFingerprint<F>(F);
39
40impl<F> LeaveFingerprint<F> {
41    /// Wraps the consuming cryptographic layer's canonical fingerprint value.
42    #[must_use]
43    pub const fn new(value: F) -> Self {
44        Self(value)
45    }
46
47    /// Borrows the consuming-layer fingerprint.
48    #[must_use]
49    pub const fn value(&self) -> &F {
50        &self.0
51    }
52
53    /// Consumes the wrapper and returns the fingerprint value.
54    #[must_use]
55    pub fn into_inner(self) -> F {
56        self.0
57    }
58}
59
60/// Complete persistence input for restoring one live member.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct LiveMemberRestore<F> {
63    /// Permanent participant identity/index.
64    pub participant_id: ParticipantId,
65    /// Owning conversation.
66    pub conversation_id: ConversationId,
67    /// Current credential generation.
68    pub generation: Generation,
69    /// Current attach secret.
70    pub attach_secret: AttachSecret,
71    /// Durable cumulative participant cursor.
72    pub cursor: DeliverySeq,
73    /// Permanent enrollment-token fingerprint.
74    pub enrollment_fingerprint: EnrollmentFingerprint<F>,
75    /// Most recent committed binding terminal, if any.
76    pub latest_terminal: Option<CommittedBindingTerminal>,
77}
78
79/// Invalid durable membership/history combination.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum MembershipInvariantError {
82    /// Retained terminal names another participant or conversation.
83    TerminalIdentity,
84    /// Retained terminal belongs to a generation newer than the current credential.
85    TerminalGeneration,
86    /// A rotation asked for a cursor BELOW the one this member already holds.
87    ///
88    /// The cursor is a durable cumulative boundary: the records this
89    /// participant has acknowledged are acknowledged, and a new generation of
90    /// the same identity does not unacknowledge them. A rotation that lowered
91    /// it would make the participant's next cumulative acknowledgement read as
92    /// a gap over records it had already accounted for, and a participant that
93    /// then withholds acknowledgements is a seat that never comes back.
94    CursorRegression,
95}
96
97/// Live participant membership plus permanent enrollment and terminal history.
98///
99/// Fields are private so a committed binding terminal cannot drift away from
100/// the identity that owns it. Persistence restoration must pass [`Self::restore`].
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct LiveMember<F> {
103    participant_id: ParticipantId,
104    conversation_id: ConversationId,
105    generation: Generation,
106    attach_secret: AttachSecret,
107    cursor: DeliverySeq,
108    enrollment_fingerprint: EnrollmentFingerprint<F>,
109    latest_terminal: Option<CommittedBindingTerminal>,
110}
111
112/// Crate-owned proof of one exact cumulative cursor transition.
113///
114/// Fields and construction remain crate-private so consuming servers can apply
115/// only an update emitted by a typed protocol operation.
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
117pub(super) struct LiveMemberCursorUpdate {
118    conversation_id: ConversationId,
119    participant_id: ParticipantId,
120    generation: Generation,
121    from_cursor: DeliverySeq,
122    resulting_cursor: DeliverySeq,
123}
124
125impl LiveMemberCursorUpdate {
126    /// Captures the complete identity and old/new cursor prestate.
127    pub(super) const fn new(
128        conversation_id: ConversationId,
129        participant_id: ParticipantId,
130        generation: Generation,
131        from_cursor: DeliverySeq,
132        resulting_cursor: DeliverySeq,
133    ) -> Self {
134        Self {
135            conversation_id,
136            participant_id,
137            generation,
138            from_cursor,
139            resulting_cursor,
140        }
141    }
142
143    pub(super) const fn previous_cursor(self) -> DeliverySeq {
144        self.from_cursor
145    }
146
147    pub(super) const fn resulting_cursor(self) -> DeliverySeq {
148        self.resulting_cursor
149    }
150}
151
152/// Rejection while applying an opaque cursor update to durable membership.
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154pub(super) enum LiveMemberCursorUpdateError {
155    /// Conversation differs from the commit proof.
156    Conversation {
157        expected: ConversationId,
158        actual: ConversationId,
159    },
160    /// Participant differs from the commit proof.
161    Participant {
162        expected: ParticipantId,
163        actual: ParticipantId,
164    },
165    /// Credential generation differs from the commit proof.
166    Generation {
167        expected: Generation,
168        actual: Generation,
169    },
170    /// Proposed update is not a strict cumulative advance.
171    NonAdvancing {
172        from_cursor: DeliverySeq,
173        resulting_cursor: DeliverySeq,
174    },
175    /// Member cursor is neither the old nor already-committed new prestate.
176    CursorPrestate {
177        expected_from_cursor: DeliverySeq,
178        resulting_cursor: DeliverySeq,
179        actual_cursor: DeliverySeq,
180    },
181}
182
183impl<F> LiveMember<F> {
184    /// Restores a durable member after checking retained terminal identity and generation.
185    ///
186    /// # Errors
187    ///
188    /// Returns [`MembershipInvariantError`] when the terminal belongs to another
189    /// identity/conversation or a generation newer than the restored credential.
190    pub fn restore(state: LiveMemberRestore<F>) -> Result<Self, MembershipInvariantError> {
191        validate_terminal(
192            state.participant_id,
193            state.conversation_id,
194            state.generation,
195            state.latest_terminal,
196        )?;
197        Ok(Self {
198            participant_id: state.participant_id,
199            conversation_id: state.conversation_id,
200            generation: state.generation,
201            attach_secret: state.attach_secret,
202            cursor: state.cursor,
203            enrollment_fingerprint: state.enrollment_fingerprint,
204            latest_terminal: state.latest_terminal,
205        })
206    }
207
208    pub(crate) const fn from_enrollment(
209        participant_id: ParticipantId,
210        conversation_id: ConversationId,
211        attach_secret: AttachSecret,
212        enrollment_fingerprint: EnrollmentFingerprint<F>,
213    ) -> Self {
214        Self {
215            participant_id,
216            conversation_id,
217            generation: Generation::ONE,
218            attach_secret,
219            cursor: 0,
220            enrollment_fingerprint,
221            latest_terminal: None,
222        }
223    }
224
225    /// Returns the permanent participant identity/index.
226    #[must_use]
227    pub const fn participant_id(&self) -> ParticipantId {
228        self.participant_id
229    }
230
231    /// Returns the owning conversation.
232    #[must_use]
233    pub const fn conversation_id(&self) -> ConversationId {
234        self.conversation_id
235    }
236
237    /// Returns the current credential generation.
238    #[must_use]
239    pub const fn generation(&self) -> Generation {
240        self.generation
241    }
242
243    /// Returns the current attach secret.
244    #[must_use]
245    pub const fn attach_secret(&self) -> AttachSecret {
246        self.attach_secret
247    }
248
249    /// Returns the durable cumulative cursor.
250    #[must_use]
251    pub const fn cursor(&self) -> DeliverySeq {
252        self.cursor
253    }
254
255    /// Borrows the permanent enrollment-token fingerprint.
256    #[must_use]
257    pub const fn enrollment_fingerprint(&self) -> &EnrollmentFingerprint<F> {
258        &self.enrollment_fingerprint
259    }
260
261    /// Returns the most recent committed binding terminal.
262    #[must_use]
263    pub const fn latest_terminal(&self) -> Option<CommittedBindingTerminal> {
264        self.latest_terminal
265    }
266
267    /// Applies an exact old/new-prestate cursor proof without permitting regression.
268    pub(super) fn apply_cursor_update(
269        &mut self,
270        update: LiveMemberCursorUpdate,
271    ) -> Result<(), LiveMemberCursorUpdateError> {
272        if self.conversation_id != update.conversation_id {
273            return Err(LiveMemberCursorUpdateError::Conversation {
274                expected: update.conversation_id,
275                actual: self.conversation_id,
276            });
277        }
278        if self.participant_id != update.participant_id {
279            return Err(LiveMemberCursorUpdateError::Participant {
280                expected: update.participant_id,
281                actual: self.participant_id,
282            });
283        }
284        if self.generation != update.generation {
285            return Err(LiveMemberCursorUpdateError::Generation {
286                expected: update.generation,
287                actual: self.generation,
288            });
289        }
290        if update.resulting_cursor <= update.from_cursor {
291            return Err(LiveMemberCursorUpdateError::NonAdvancing {
292                from_cursor: update.from_cursor,
293                resulting_cursor: update.resulting_cursor,
294            });
295        }
296        if self.cursor == update.resulting_cursor {
297            return Ok(());
298        }
299        if self.cursor != update.from_cursor {
300            return Err(LiveMemberCursorUpdateError::CursorPrestate {
301                expected_from_cursor: update.from_cursor,
302                resulting_cursor: update.resulting_cursor,
303                actual_cursor: self.cursor,
304            });
305        }
306        self.cursor = update.resulting_cursor;
307        Ok(())
308    }
309
310    /// Replaces the latest binding terminal after checking its identity domain.
311    ///
312    /// # Errors
313    ///
314    /// Returns [`MembershipInvariantError`] for mismatched identity or generation.
315    pub fn with_committed_terminal(
316        mut self,
317        terminal: CommittedBindingTerminal,
318    ) -> Result<Self, MembershipInvariantError> {
319        validate_terminal(
320            self.participant_id,
321            self.conversation_id,
322            self.generation,
323            Some(terminal),
324        )?;
325        self.latest_terminal = Some(terminal);
326        Ok(self)
327    }
328
329    pub(super) fn rotate(
330        mut self,
331        generation: Generation,
332        attach_secret: AttachSecret,
333        cursor: DeliverySeq,
334        terminal: Option<CommittedBindingTerminal>,
335    ) -> Result<Self, MembershipInvariantError> {
336        let latest_terminal = terminal.or(self.latest_terminal);
337        validate_terminal(
338            self.participant_id,
339            self.conversation_id,
340            generation,
341            latest_terminal,
342        )?;
343        if cursor < self.cursor {
344            return Err(MembershipInvariantError::CursorRegression);
345        }
346        self.generation = generation;
347        self.attach_secret = attach_secret;
348        self.cursor = cursor;
349        self.latest_terminal = latest_terminal;
350        Ok(self)
351    }
352}
353
354fn validate_terminal(
355    participant_id: ParticipantId,
356    conversation_id: ConversationId,
357    generation: Generation,
358    terminal: Option<CommittedBindingTerminal>,
359) -> Result<(), MembershipInvariantError> {
360    let Some(terminal) = terminal else {
361        return Ok(());
362    };
363    if terminal.participant_id() != participant_id || terminal.conversation_id() != conversation_id
364    {
365        return Err(MembershipInvariantError::TerminalIdentity);
366    }
367    if terminal.binding_epoch().capability_generation > generation {
368        return Err(MembershipInvariantError::TerminalGeneration);
369    }
370    Ok(())
371}
372
373/// Permanent retired identity tombstone.
374///
375/// The tombstone retains generic non-reversible fingerprints/verifier but no
376/// attach secret or request body.
377#[derive(Clone, Debug, PartialEq, Eq)]
378pub struct RetiredIdentity<EF, V, LF> {
379    participant_id: ParticipantId,
380    conversation_id: ConversationId,
381    retired_generation: Generation,
382    enrollment_fingerprint: EnrollmentFingerprint<EF>,
383    leave_attempt_token: LeaveAttemptToken,
384    leave_request_verifier: V,
385    leave_fingerprint: LeaveFingerprint<LF>,
386    left_admission_order: AdmissionOrder,
387    committed_result: LeaveCommitted,
388}
389
390impl<EF, V, LF> RetiredIdentity<EF, V, LF> {
391    /// Permanent participant id.
392    #[must_use]
393    pub const fn participant_id(&self) -> ParticipantId {
394        self.participant_id
395    }
396
397    /// Conversation containing the tombstone.
398    #[must_use]
399    pub const fn conversation_id(&self) -> ConversationId {
400        self.conversation_id
401    }
402
403    /// Permanent retired generation.
404    #[must_use]
405    pub const fn retired_generation(&self) -> Generation {
406        self.retired_generation
407    }
408
409    /// Permanent committed Leave token.
410    #[must_use]
411    pub const fn leave_attempt_token(&self) -> LeaveAttemptToken {
412        self.leave_attempt_token
413    }
414
415    /// Stored complete Leave result for exact replay.
416    #[must_use]
417    pub const fn committed_result(&self) -> &LeaveCommitted {
418        &self.committed_result
419    }
420
421    /// Returns the immutable causal key of the permanent `Left` record.
422    #[must_use]
423    pub const fn left_admission_order(&self) -> AdmissionOrder {
424        self.left_admission_order
425    }
426
427    /// Stored non-reversible secret-proof verifier.
428    #[must_use]
429    pub const fn leave_request_verifier(&self) -> &V {
430        &self.leave_request_verifier
431    }
432
433    /// Permanent enrollment mapping fingerprint.
434    #[must_use]
435    pub const fn enrollment_fingerprint(&self) -> &EnrollmentFingerprint<EF> {
436        &self.enrollment_fingerprint
437    }
438
439    /// Permanent canonical Leave fingerprint.
440    #[must_use]
441    pub const fn leave_fingerprint(&self) -> &LeaveFingerprint<LF> {
442        &self.leave_fingerprint
443    }
444
445    #[allow(clippy::too_many_arguments)]
446    pub(super) fn restore(
447        participant_id: ParticipantId,
448        conversation_id: ConversationId,
449        retired_generation: Generation,
450        enrollment_fingerprint: EnrollmentFingerprint<EF>,
451        leave_attempt_token: LeaveAttemptToken,
452        leave_request_verifier: V,
453        leave_fingerprint: LeaveFingerprint<LF>,
454        left_transaction_order: TransactionOrder,
455        committed_result: LeaveCommitted,
456    ) -> Result<Self, RetirementError> {
457        if committed_result.conversation_id() != conversation_id {
458            return Err(RetirementError::Conversation);
459        }
460        if committed_result.participant_id() != participant_id {
461            return Err(RetirementError::Participant);
462        }
463        if committed_result.presented_generation() != retired_generation {
464            return Err(RetirementError::Generation);
465        }
466        if committed_result.retired_generation() != retired_generation {
467            return Err(RetirementError::RetiredGeneration);
468        }
469        if committed_result.leave_attempt_token() != leave_attempt_token {
470            return Err(RetirementError::Token);
471        }
472        let left_admission_order = AdmissionOrder::new(
473            left_transaction_order,
474            CandidatePhase::MembershipExit,
475            participant_id,
476        );
477        Ok(Self {
478            participant_id,
479            conversation_id,
480            retired_generation,
481            enrollment_fingerprint,
482            leave_attempt_token,
483            leave_request_verifier,
484            leave_fingerprint,
485            left_admission_order,
486            committed_result,
487        })
488    }
489}
490
491/// Present participant identity state; absence is represented outside this enum.
492#[derive(Clone, Debug, PartialEq, Eq)]
493pub enum IdentityState<EF, V, LF> {
494    /// Live membership, whether bound or detached.
495    Live(LiveMember<EF>),
496    /// Permanent Leave tombstone.
497    Retired(RetiredIdentity<EF, V, LF>),
498}
499
500/// One indivisible committed Leave state update.
501///
502/// The identity tombstone and the claim frontiers resulting from the prepared
503/// Leave transition are deliberately carried together and are not cloneable.
504/// A durable binding must persist both parts in the same transaction; exposing
505/// only the tombstone would discard the consumed and relayed frontier authority.
506#[derive(Debug, PartialEq, Eq)]
507pub struct LeaveCommit<EF, V, LF> {
508    identity: IdentityState<EF, V, LF>,
509    frontiers: ClaimFrontiers,
510}
511
512impl<EF, V, LF> LeaveCommit<EF, V, LF> {
513    /// Borrows the permanent identity result committed by Leave.
514    #[must_use]
515    pub const fn identity(&self) -> &IdentityState<EF, V, LF> {
516        &self.identity
517    }
518
519    /// Borrows the exact claim frontiers committed with the identity result.
520    #[must_use]
521    pub const fn frontiers(&self) -> &ClaimFrontiers {
522        &self.frontiers
523    }
524
525    /// Consumes the atomic commit for one durable transaction.
526    #[must_use]
527    pub fn into_parts(self) -> (IdentityState<EF, V, LF>, ClaimFrontiers) {
528        (self.identity, self.frontiers)
529    }
530}
531
532/// Mismatch between a live member and proposed stored Leave result.
533#[derive(Clone, Copy, Debug, PartialEq, Eq)]
534pub enum RetirementError {
535    /// Result names another conversation.
536    Conversation,
537    /// Result names another participant.
538    Participant,
539    /// Result's presented generation differs from current generation.
540    Generation,
541    /// Result's retired generation differs from the current live generation.
542    RetiredGeneration,
543    /// Result's Leave token differs from the committing token.
544    Token,
545    /// Stored `Left` order is not phase 1 for the retired participant.
546    LeftAdmissionOrder,
547}
548
549/// Failure while proving a live member's exact Leave request authority.
550#[derive(Clone, Copy, Debug, PartialEq, Eq)]
551pub enum LeaveVerificationError {
552    /// Request names another conversation.
553    Conversation,
554    /// Request names another participant.
555    Participant,
556    /// Presented generation is not the current live generation.
557    Generation,
558    /// Presented attach secret failed the consuming layer's constant-time proof.
559    Secret,
560}
561
562/// Exact Leave request authority proven against one live member.
563pub struct VerifiedLeaveRequest<V, LF> {
564    conversation_id: ConversationId,
565    participant_id: ParticipantId,
566    generation: Generation,
567    leave_attempt_token: LeaveAttemptToken,
568    leave_request_verifier: V,
569    leave_fingerprint: LeaveFingerprint<LF>,
570}
571
572/// Allocation fields for settled bound or detached Leave.
573#[derive(Clone, Copy, Debug, PartialEq, Eq)]
574pub struct LeaveCommitParameters {
575    /// Assigned terminal `Left` record sequence.
576    pub left_delivery_seq: DeliverySeq,
577}
578
579/// Proof bound to one exact pending finalization and later `Left` major.
580#[derive(Debug, PartialEq, Eq)]
581struct NoInterveningTupleProof {
582    pending_order: AdmissionOrder,
583    left_transaction_order: TransactionOrder,
584}
585
586impl NoInterveningTupleProof {
587    fn matches(&self, pending: PendingFinalization) -> bool {
588        self.pending_order == pending.admission_order()
589            && self.left_transaction_order > self.pending_order.transaction_order()
590    }
591}
592
593/// Linear order-lane authority for one exact settled or positional Leave.
594///
595/// Construction consumes a completely validated [`ClaimFrontiers`] snapshot,
596/// removes and relays the exact `X` order handle, and seals the resulting lane
597/// inside this value. The frontier cannot be recovered by a caller or reused
598/// after a successful commit. Leave returns that authority only inside the
599/// indivisible [`LeaveCommit`] beside the resulting tombstone.
600///
601/// External code cannot implement a planner proof or initialize this authority
602/// from raw majors:
603///
604/// ```compile_fail
605/// use liminal_protocol::lifecycle::NoInterveningTuplePlannerProof;
606/// ```
607///
608/// ```compile_fail
609/// use liminal_protocol::lifecycle::PreparedLeaveAuthority;
610///
611/// let _ = PreparedLeaveAuthority { left_transaction_order: 7 };
612/// ```
613#[derive(Debug, PartialEq, Eq)]
614pub struct PreparedLeaveAuthority {
615    frontiers: ClaimFrontiers,
616    conversation_id: ConversationId,
617    participant_id: ParticipantId,
618    kind: PreparedLeaveKind,
619}
620
621#[derive(Debug, PartialEq, Eq)]
622enum PreparedLeaveKind {
623    Settled {
624        ended_binding_epoch: Option<BindingEpoch>,
625        left_transaction_order: TransactionOrder,
626    },
627    Pending {
628        binding_epoch: BindingEpoch,
629        no_intervening: NoInterveningTupleProof,
630    },
631}
632
633impl PreparedLeaveAuthority {
634    pub(super) const fn settled(
635        frontiers: ClaimFrontiers,
636        conversation_id: ConversationId,
637        participant_id: ParticipantId,
638        ended_binding_epoch: Option<BindingEpoch>,
639        left_transaction_order: TransactionOrder,
640    ) -> Self {
641        Self {
642            frontiers,
643            conversation_id,
644            participant_id,
645            kind: PreparedLeaveKind::Settled {
646                ended_binding_epoch,
647                left_transaction_order,
648            },
649        }
650    }
651
652    pub(super) const fn pending(
653        frontiers: ClaimFrontiers,
654        conversation_id: ConversationId,
655        participant_id: ParticipantId,
656        binding_epoch: BindingEpoch,
657        pending_order: AdmissionOrder,
658        left_transaction_order: TransactionOrder,
659    ) -> Self {
660        Self {
661            frontiers,
662            conversation_id,
663            participant_id,
664            kind: PreparedLeaveKind::Pending {
665                binding_epoch,
666                no_intervening: NoInterveningTupleProof {
667                    pending_order,
668                    left_transaction_order,
669                },
670            },
671        }
672    }
673
674    fn consume_settled(
675        self,
676        member_conversation_id: ConversationId,
677        member_participant_id: ParticipantId,
678        ended_binding_epoch: Option<BindingEpoch>,
679    ) -> Result<(ClaimFrontiers, TransactionOrder), LeaveCommitError> {
680        let Self {
681            frontiers,
682            conversation_id,
683            participant_id,
684            kind,
685        } = self;
686        let PreparedLeaveKind::Settled {
687            ended_binding_epoch: authorized_epoch,
688            left_transaction_order,
689        } = kind
690        else {
691            return Err(LeaveCommitError::PreparedAuthority);
692        };
693        if conversation_id != member_conversation_id
694            || participant_id != member_participant_id
695            || authorized_epoch != ended_binding_epoch
696        {
697            return Err(LeaveCommitError::PreparedAuthority);
698        }
699        Ok((frontiers, left_transaction_order))
700    }
701
702    fn consume_pending(
703        self,
704        pending: PendingFinalization,
705    ) -> Result<(ClaimFrontiers, TransactionOrder), LeaveCommitError> {
706        let Self {
707            frontiers,
708            conversation_id,
709            participant_id,
710            kind,
711        } = self;
712        let PreparedLeaveKind::Pending {
713            binding_epoch,
714            no_intervening,
715        } = kind
716        else {
717            return Err(LeaveCommitError::PreparedAuthority);
718        };
719        if conversation_id != pending.conversation_id()
720            || participant_id != pending.participant_id()
721            || binding_epoch != pending.binding_epoch()
722            || !no_intervening.matches(pending)
723        {
724            return Err(LeaveCommitError::PreparedAuthority);
725        }
726        Ok((frontiers, no_intervening.left_transaction_order))
727    }
728}
729
730/// Allocation and ordering proof for positional pending-terminal Leave.
731#[derive(Clone, Copy, Debug, PartialEq, Eq)]
732pub struct PendingLeaveCommitParameters {
733    /// Real sequence allocated to the pending binding terminal.
734    pub terminal_delivery_seq: DeliverySeq,
735    /// Real sequence allocated to the following `Left` record.
736    pub left_delivery_seq: DeliverySeq,
737}
738
739/// Failure while applying an already-authorized Leave transaction.
740#[derive(Clone, Copy, Debug, PartialEq, Eq)]
741pub enum LeaveCommitError {
742    /// Prepared order authority belongs to another state or Leave mode.
743    PreparedAuthority,
744    /// Verified request authority was minted for another live member.
745    VerifiedAuthority,
746    /// Bound or pending-finalization state belongs to another member/generation.
747    BindingAuthority,
748    /// Settled Leave was called while a binding terminal remains pending.
749    PendingTerminalRequiresComposition,
750    /// Positional proof does not cover the supplied pending finalization.
751    NoInterveningTuple,
752    /// A pending detach cell is not paired with explicit-detach finalization.
753    PendingDetachState,
754    /// Detach cell and retained committed terminal disagree.
755    TerminalHistory,
756    /// Prior terminal sequence is not strictly before the new `Left` sequence.
757    TerminalSequenceOrder,
758    /// The supplied record positions do not consume the next gap-free sequence values.
759    SequenceAuthority,
760    /// Consuming and relaying Leave claims could not produce a valid frontier.
761    ResultingFrontier,
762    /// Internal tombstone construction rejected an inconsistent result.
763    RetirementInvariant(RetirementError),
764}
765
766impl<F> LiveMember<F> {
767    /// Verifies an exact Leave request against current live credential authority.
768    ///
769    /// # Errors
770    ///
771    /// Returns [`LeaveVerificationError`] at the first mismatching authority component.
772    pub fn verify_leave_request<V, LF>(
773        &self,
774        request: &LeaveRequest,
775        secret_proof: AttachSecretProof,
776        leave_request_verifier: V,
777        leave_fingerprint: LeaveFingerprint<LF>,
778    ) -> Result<VerifiedLeaveRequest<V, LF>, LeaveVerificationError> {
779        if request.conversation_id != self.conversation_id {
780            return Err(LeaveVerificationError::Conversation);
781        }
782        if request.participant_id != self.participant_id {
783            return Err(LeaveVerificationError::Participant);
784        }
785        if request.capability_generation != self.generation {
786            return Err(LeaveVerificationError::Generation);
787        }
788        if secret_proof == AttachSecretProof::Mismatch {
789            return Err(LeaveVerificationError::Secret);
790        }
791        Ok(VerifiedLeaveRequest {
792            conversation_id: request.conversation_id,
793            participant_id: request.participant_id,
794            generation: request.capability_generation,
795            leave_attempt_token: request.leave_attempt_token,
796            leave_request_verifier,
797            leave_fingerprint,
798        })
799    }
800
801    fn retire<V, LF>(
802        self,
803        leave_attempt_token: LeaveAttemptToken,
804        leave_request_verifier: V,
805        leave_fingerprint: LeaveFingerprint<LF>,
806        left_admission_order: AdmissionOrder,
807        committed_result: LeaveCommitted,
808    ) -> Result<RetiredIdentity<F, V, LF>, RetirementError> {
809        if committed_result.conversation_id() != self.conversation_id {
810            return Err(RetirementError::Conversation);
811        }
812        if committed_result.participant_id() != self.participant_id {
813            return Err(RetirementError::Participant);
814        }
815        if committed_result.presented_generation() != self.generation {
816            return Err(RetirementError::Generation);
817        }
818        if committed_result.retired_generation() != self.generation {
819            return Err(RetirementError::RetiredGeneration);
820        }
821        if committed_result.leave_attempt_token() != leave_attempt_token {
822            return Err(RetirementError::Token);
823        }
824        if left_admission_order.candidate_phase() != CandidatePhase::MembershipExit
825            || left_admission_order.participant_index() != self.participant_id
826        {
827            return Err(RetirementError::LeftAdmissionOrder);
828        }
829        Ok(RetiredIdentity {
830            participant_id: self.participant_id,
831            conversation_id: self.conversation_id,
832            retired_generation: committed_result.retired_generation(),
833            enrollment_fingerprint: self.enrollment_fingerprint,
834            leave_attempt_token,
835            leave_request_verifier,
836            leave_fingerprint,
837            left_admission_order,
838            committed_result,
839        })
840    }
841}
842
843/// Commits bound or already-detached Leave, deriving all optional result fields.
844///
845/// # Errors
846///
847/// Returns [`LeaveCommitError`] when authority, binding, history/cell, or order
848/// is inconsistent. Pending finalization must use [`commit_pending_leave`].
849pub fn commit_leave<EF, V, LF, D>(
850    member: LiveMember<EF>,
851    binding_state: BindingState,
852    detach_cell: DetachCell<D>,
853    verified: VerifiedLeaveRequest<V, LF>,
854    authority: PreparedLeaveAuthority,
855    parameters: LeaveCommitParameters,
856) -> Result<LeaveCommit<EF, V, LF>, LeaveCommitError> {
857    validate_verified(&member, &verified)?;
858    let ended_binding_epoch = match binding_state {
859        BindingState::Detached => None,
860        BindingState::Bound(active) => {
861            validate_active(
862                &member,
863                active.participant_id,
864                active.conversation_id,
865                active.binding_epoch,
866            )?;
867            Some(active.binding_epoch)
868        }
869        BindingState::PendingFinalization(_) => {
870            return Err(LeaveCommitError::PendingTerminalRequiresComposition);
871        }
872    };
873    validate_settled_cell(&member, binding_state, &detach_cell)?;
874    let (frontiers, left_transaction_order) = authority.consume_settled(
875        member.conversation_id,
876        member.participant_id,
877        ended_binding_epoch,
878    )?;
879    let prior_terminal_delivery_seq = member
880        .latest_terminal
881        .map(CommittedBindingTerminal::delivery_seq);
882    validate_sequence_order(prior_terminal_delivery_seq, parameters.left_delivery_seq)?;
883    finish_leave(
884        member,
885        verified,
886        ended_binding_epoch,
887        prior_terminal_delivery_seq,
888        None,
889        left_transaction_order,
890        parameters.left_delivery_seq,
891        detach_cell,
892        frontiers,
893    )
894}
895
896/// Positionally commits one pending binding terminal immediately before `Left`.
897///
898/// A separately drained terminal must first update [`LiveMember`] through its
899/// committed terminal and then use ordinary [`commit_leave`].
900///
901/// # Errors
902///
903/// Returns [`LeaveCommitError`] when the planner proof, pending state/cell,
904/// authority, or allocated sequence order is inconsistent.
905pub fn commit_pending_leave<EF, V, LF, D>(
906    member: LiveMember<EF>,
907    pending: PendingFinalization,
908    detach_cell: DetachCell<D>,
909    verified: VerifiedLeaveRequest<V, LF>,
910    authority: PreparedLeaveAuthority,
911    parameters: PendingLeaveCommitParameters,
912) -> Result<LeaveCommit<EF, V, LF>, LeaveCommitError> {
913    let PendingLeaveCommitParameters {
914        terminal_delivery_seq,
915        left_delivery_seq,
916    } = parameters;
917    validate_verified(&member, &verified)?;
918    validate_pending(&member, pending)?;
919    let (frontiers, left_transaction_order) = authority.consume_pending(pending)?;
920    validate_pending_cell(member.conversation_id, pending, &detach_cell)?;
921    if terminal_delivery_seq >= left_delivery_seq {
922        return Err(LeaveCommitError::TerminalSequenceOrder);
923    }
924    let committed_terminal = pending.commit(terminal_delivery_seq);
925    validate_terminal(
926        member.participant_id,
927        member.conversation_id,
928        member.generation,
929        Some(committed_terminal),
930    )
931    .map_err(|_| LeaveCommitError::BindingAuthority)?;
932    finish_leave(
933        member,
934        verified,
935        None,
936        Some(committed_terminal.delivery_seq()),
937        Some(committed_terminal),
938        left_transaction_order,
939        left_delivery_seq,
940        detach_cell,
941        frontiers,
942    )
943}
944
945fn validate_verified<EF, V, LF>(
946    member: &LiveMember<EF>,
947    verified: &VerifiedLeaveRequest<V, LF>,
948) -> Result<(), LeaveCommitError> {
949    if verified.conversation_id != member.conversation_id
950        || verified.participant_id != member.participant_id
951        || verified.generation != member.generation
952    {
953        return Err(LeaveCommitError::VerifiedAuthority);
954    }
955    Ok(())
956}
957
958fn validate_active<EF>(
959    member: &LiveMember<EF>,
960    participant_id: ParticipantId,
961    conversation_id: ConversationId,
962    binding_epoch: BindingEpoch,
963) -> Result<(), LeaveCommitError> {
964    if participant_id != member.participant_id
965        || conversation_id != member.conversation_id
966        || binding_epoch.capability_generation != member.generation
967    {
968        return Err(LeaveCommitError::BindingAuthority);
969    }
970    Ok(())
971}
972
973fn validate_pending<EF>(
974    member: &LiveMember<EF>,
975    pending: PendingFinalization,
976) -> Result<(), LeaveCommitError> {
977    validate_active(
978        member,
979        pending.participant_id(),
980        pending.conversation_id(),
981        pending.binding_epoch(),
982    )
983}
984
985fn validate_sequence_order(
986    prior: Option<DeliverySeq>,
987    left: DeliverySeq,
988) -> Result<(), LeaveCommitError> {
989    if prior.is_some_and(|sequence| sequence >= left) {
990        return Err(LeaveCommitError::TerminalSequenceOrder);
991    }
992    Ok(())
993}
994
995fn validate_settled_cell<EF, D>(
996    member: &LiveMember<EF>,
997    binding_state: BindingState,
998    detach_cell: &DetachCell<D>,
999) -> Result<(), LeaveCommitError> {
1000    match detach_cell {
1001        DetachCell::Empty(_) => Ok(()),
1002        DetachCell::Pending(_) => Err(LeaveCommitError::PendingDetachState),
1003        DetachCell::Committed(cell) => {
1004            if binding_state != BindingState::Detached
1005                || cell.participant_id() != member.participant_id
1006                || cell.request_generation() != member.generation
1007            {
1008                return Err(LeaveCommitError::TerminalHistory);
1009            }
1010            let Some(terminal) = member.latest_terminal else {
1011                return Err(LeaveCommitError::TerminalHistory);
1012            };
1013            if terminal.detached_cause() != Some(DetachedCause::CleanDeregister)
1014                || terminal.binding_epoch() != cell.committed_binding_epoch()
1015                || terminal.delivery_seq() != cell.detached_delivery_seq()
1016            {
1017                return Err(LeaveCommitError::TerminalHistory);
1018            }
1019            Ok(())
1020        }
1021        DetachCell::Terminalized(cell) => {
1022            if cell.participant_id() != member.participant_id || member.latest_terminal.is_none() {
1023                return Err(LeaveCommitError::TerminalHistory);
1024            }
1025            Ok(())
1026        }
1027    }
1028}
1029
1030fn validate_pending_cell<D>(
1031    conversation_id: ConversationId,
1032    pending: PendingFinalization,
1033    detach_cell: &DetachCell<D>,
1034) -> Result<(), LeaveCommitError> {
1035    match detach_cell {
1036        DetachCell::Pending(cell) => validate_pending_pair(
1037            BindingState::PendingFinalization(pending),
1038            cell,
1039            Some(conversation_id),
1040        )
1041        .map(|_| ())
1042        .map_err(|_| LeaveCommitError::PendingDetachState),
1043        DetachCell::Committed(_) => Err(LeaveCommitError::TerminalHistory),
1044        DetachCell::Terminalized(cell) if cell.participant_id() != pending.participant_id() => {
1045            Err(LeaveCommitError::TerminalHistory)
1046        }
1047        DetachCell::Empty(_) | DetachCell::Terminalized(_) => Ok(()),
1048    }
1049}
1050
1051#[allow(
1052    clippy::too_many_arguments,
1053    reason = "the final Leave constructor keeps every verified authority and both atomic result halves explicit"
1054)]
1055fn finish_leave<EF, V, LF, D>(
1056    member: LiveMember<EF>,
1057    verified: VerifiedLeaveRequest<V, LF>,
1058    ended_binding_epoch: Option<BindingEpoch>,
1059    prior_terminal_delivery_seq: Option<DeliverySeq>,
1060    committed_terminal: Option<CommittedBindingTerminal>,
1061    left_transaction_order: TransactionOrder,
1062    left_delivery_seq: DeliverySeq,
1063    detach_cell: DetachCell<D>,
1064    frontiers: ClaimFrontiers,
1065) -> Result<LeaveCommit<EF, V, LF>, LeaveCommitError> {
1066    let VerifiedLeaveRequest {
1067        conversation_id,
1068        participant_id,
1069        generation,
1070        leave_attempt_token,
1071        leave_request_verifier,
1072        leave_fingerprint,
1073    } = verified;
1074    if generation != member.generation {
1075        return Err(LeaveCommitError::VerifiedAuthority);
1076    }
1077    let Some(committed_result) = LeaveCommitted::new(
1078        conversation_id,
1079        leave_attempt_token,
1080        participant_id,
1081        member.generation,
1082        ended_binding_epoch,
1083        prior_terminal_delivery_seq,
1084        left_delivery_seq,
1085    ) else {
1086        return Err(LeaveCommitError::TerminalSequenceOrder);
1087    };
1088    let frontiers = frontiers.finish_leave_claims(
1089        participant_id,
1090        ended_binding_epoch,
1091        committed_terminal,
1092        left_delivery_seq,
1093        left_transaction_order,
1094    )?;
1095    let retired = member
1096        .retire(
1097            leave_attempt_token,
1098            leave_request_verifier,
1099            leave_fingerprint,
1100            AdmissionOrder::new(
1101                left_transaction_order,
1102                CandidatePhase::MembershipExit,
1103                participant_id,
1104            ),
1105            committed_result,
1106        )
1107        .map_err(LeaveCommitError::RetirementInvariant)?;
1108    let _detach_cell_replaced_by_tombstone = detach_cell;
1109    Ok(LeaveCommit {
1110        identity: IdentityState::Retired(retired),
1111        frontiers,
1112    })
1113}