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#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct EnrollmentFingerprint<F>(F);
15
16impl<F> EnrollmentFingerprint<F> {
17 #[must_use]
19 pub const fn new(value: F) -> Self {
20 Self(value)
21 }
22
23 #[must_use]
25 pub const fn value(&self) -> &F {
26 &self.0
27 }
28
29 #[must_use]
31 pub fn into_inner(self) -> F {
32 self.0
33 }
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct LeaveFingerprint<F>(F);
39
40impl<F> LeaveFingerprint<F> {
41 #[must_use]
43 pub const fn new(value: F) -> Self {
44 Self(value)
45 }
46
47 #[must_use]
49 pub const fn value(&self) -> &F {
50 &self.0
51 }
52
53 #[must_use]
55 pub fn into_inner(self) -> F {
56 self.0
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct LiveMemberRestore<F> {
63 pub participant_id: ParticipantId,
65 pub conversation_id: ConversationId,
67 pub generation: Generation,
69 pub attach_secret: AttachSecret,
71 pub cursor: DeliverySeq,
73 pub enrollment_fingerprint: EnrollmentFingerprint<F>,
75 pub latest_terminal: Option<CommittedBindingTerminal>,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum MembershipInvariantError {
82 TerminalIdentity,
84 TerminalGeneration,
86 CursorRegression,
95}
96
97#[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#[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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154pub(super) enum LiveMemberCursorUpdateError {
155 Conversation {
157 expected: ConversationId,
158 actual: ConversationId,
159 },
160 Participant {
162 expected: ParticipantId,
163 actual: ParticipantId,
164 },
165 Generation {
167 expected: Generation,
168 actual: Generation,
169 },
170 NonAdvancing {
172 from_cursor: DeliverySeq,
173 resulting_cursor: DeliverySeq,
174 },
175 CursorPrestate {
177 expected_from_cursor: DeliverySeq,
178 resulting_cursor: DeliverySeq,
179 actual_cursor: DeliverySeq,
180 },
181}
182
183impl<F> LiveMember<F> {
184 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 #[must_use]
227 pub const fn participant_id(&self) -> ParticipantId {
228 self.participant_id
229 }
230
231 #[must_use]
233 pub const fn conversation_id(&self) -> ConversationId {
234 self.conversation_id
235 }
236
237 #[must_use]
239 pub const fn generation(&self) -> Generation {
240 self.generation
241 }
242
243 #[must_use]
245 pub const fn attach_secret(&self) -> AttachSecret {
246 self.attach_secret
247 }
248
249 #[must_use]
251 pub const fn cursor(&self) -> DeliverySeq {
252 self.cursor
253 }
254
255 #[must_use]
257 pub const fn enrollment_fingerprint(&self) -> &EnrollmentFingerprint<F> {
258 &self.enrollment_fingerprint
259 }
260
261 #[must_use]
263 pub const fn latest_terminal(&self) -> Option<CommittedBindingTerminal> {
264 self.latest_terminal
265 }
266
267 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 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#[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 #[must_use]
393 pub const fn participant_id(&self) -> ParticipantId {
394 self.participant_id
395 }
396
397 #[must_use]
399 pub const fn conversation_id(&self) -> ConversationId {
400 self.conversation_id
401 }
402
403 #[must_use]
405 pub const fn retired_generation(&self) -> Generation {
406 self.retired_generation
407 }
408
409 #[must_use]
411 pub const fn leave_attempt_token(&self) -> LeaveAttemptToken {
412 self.leave_attempt_token
413 }
414
415 #[must_use]
417 pub const fn committed_result(&self) -> &LeaveCommitted {
418 &self.committed_result
419 }
420
421 #[must_use]
423 pub const fn left_admission_order(&self) -> AdmissionOrder {
424 self.left_admission_order
425 }
426
427 #[must_use]
429 pub const fn leave_request_verifier(&self) -> &V {
430 &self.leave_request_verifier
431 }
432
433 #[must_use]
435 pub const fn enrollment_fingerprint(&self) -> &EnrollmentFingerprint<EF> {
436 &self.enrollment_fingerprint
437 }
438
439 #[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#[derive(Clone, Debug, PartialEq, Eq)]
493pub enum IdentityState<EF, V, LF> {
494 Live(LiveMember<EF>),
496 Retired(RetiredIdentity<EF, V, LF>),
498}
499
500#[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 #[must_use]
515 pub const fn identity(&self) -> &IdentityState<EF, V, LF> {
516 &self.identity
517 }
518
519 #[must_use]
521 pub const fn frontiers(&self) -> &ClaimFrontiers {
522 &self.frontiers
523 }
524
525 #[must_use]
527 pub fn into_parts(self) -> (IdentityState<EF, V, LF>, ClaimFrontiers) {
528 (self.identity, self.frontiers)
529 }
530}
531
532#[derive(Clone, Copy, Debug, PartialEq, Eq)]
534pub enum RetirementError {
535 Conversation,
537 Participant,
539 Generation,
541 RetiredGeneration,
543 Token,
545 LeftAdmissionOrder,
547}
548
549#[derive(Clone, Copy, Debug, PartialEq, Eq)]
551pub enum LeaveVerificationError {
552 Conversation,
554 Participant,
556 Generation,
558 Secret,
560}
561
562pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
574pub struct LeaveCommitParameters {
575 pub left_delivery_seq: DeliverySeq,
577}
578
579#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
732pub struct PendingLeaveCommitParameters {
733 pub terminal_delivery_seq: DeliverySeq,
735 pub left_delivery_seq: DeliverySeq,
737}
738
739#[derive(Clone, Copy, Debug, PartialEq, Eq)]
741pub enum LeaveCommitError {
742 PreparedAuthority,
744 VerifiedAuthority,
746 BindingAuthority,
748 PendingTerminalRequiresComposition,
750 NoInterveningTuple,
752 PendingDetachState,
754 TerminalHistory,
756 TerminalSequenceOrder,
758 SequenceAuthority,
760 ResultingFrontier,
762 RetirementInvariant(RetirementError),
764}
765
766impl<F> LiveMember<F> {
767 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
843pub 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
896pub 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}