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