Skip to main content

liminal_protocol/lifecycle/
attach.rs

1use alloc::boxed::Box;
2
3use crate::wire::{
4    AttachBound, AttachSecret, BindingEpoch, CredentialAttachRequest, DeliverySeq, Generation,
5};
6
7use super::{
8    ActiveBinding, AttachedLifecycleRecord, AttachedRecordPosition, BindingOrigin, BindingState,
9    ClosureState, CommittedBindingTerminal, CommittedBindingTerminalPosition,
10    CommittedDetachedTerminal, CommittedDiedTerminal, DetachCell, Event, FencedAttachCommit,
11    LiveMember, MembershipInvariantError, ObserverProgressProjection, OrdinaryBindingAuthority,
12    OrdinaryBindingFate, OrdinaryDetachedAttachAdmission, PendingFinalization,
13    detach::validate_pending_pair, lookup::AttachSecretProof,
14};
15
16/// Result allocation owned by one successful credential-attach transaction.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct AttachCommitParameters {
19    /// Newly committed binding epoch.
20    pub binding: ActiveBinding,
21    /// Newly minted result attach secret.
22    pub attach_secret: AttachSecret,
23    /// Assigned `Attached` lifecycle record position.
24    pub attached_position: AttachedRecordPosition,
25    /// Live receipt deadline.
26    pub receipt_expires_at: u128,
27    /// Provenance deadline.
28    pub provenance_expires_at: u128,
29}
30
31/// Failure while proving credential-attach authority before commit.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum AttachVerificationError {
34    /// Request names another conversation.
35    Conversation,
36    /// Request names another participant.
37    Participant,
38    /// Request generation is not the current live generation.
39    Generation,
40    /// Presented attach secret failed the constant-time verifier.
41    Secret,
42    /// Current binding state cannot execute the selected attach mode.
43    BindingState,
44    /// Ordinary/superseding attach presented a marker, or fenced proof differed.
45    MarkerProof,
46    /// Fenced proof names another participant or binding epoch.
47    RecoveryAuthority,
48    /// Pending finalization lacks a real terminal sequence or appears in another mode.
49    PendingTerminalSequence,
50    /// Result binding does not name this member.
51    ResultBinding,
52    /// Current generation is exhausted or result generation is not its successor.
53    ResultGeneration,
54    /// Supersession terminal and Attached record do not share one transaction major.
55    LifecycleOrder,
56    /// Retained committed-terminal history cannot prove the detached recovery epoch.
57    TerminalHistory,
58}
59
60/// Failure while atomically applying a previously verified attach.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum AttachCommitError {
63    /// A pending detach is not paired with fenced pending-finalization commit.
64    PendingDetach,
65    /// Detach cell belongs to another participant or generation.
66    DetachCellAuthority,
67    /// Binding finalization and pending detach cell do not describe one commit.
68    BindingCellState,
69    /// Committed detach cell and retained terminal history disagree.
70    TerminalHistory,
71    /// Rotated membership rejected the committed terminal.
72    MembershipInvariant(MembershipInvariantError),
73    /// Canonical attach receipt rejected the verified transaction.
74    ReceiptInvariant,
75}
76
77/// Binding-terminal effect selected by one successful attach.
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum AttachTransition {
80    /// Already-detached member bound without creating another terminal.
81    Detached,
82    /// Old active epoch committed `Detached(Superseded)` in the handoff.
83    Superseded {
84        /// Exact old terminal including its real delivery sequence.
85        terminal: CommittedDetachedTerminal,
86    },
87    /// Marker-fenced recovery accepted the delivered marker atomically.
88    FencedRecovery {
89        /// Exact dead epoch that durably received the marker.
90        prior_binding_epoch: BindingEpoch,
91        /// Terminal committed from pending state in this transaction, if any.
92        composed_terminal: Option<CommittedBindingTerminal>,
93        /// Closure successor restricted to clear, projection, or compaction.
94        next_closure_state: ClosureState,
95    },
96}
97
98/// Complete atomic result of a credential attach.
99#[derive(Debug, PartialEq, Eq)]
100pub struct AttachCommit<F, V> {
101    /// Rotated membership with preserved or replaced terminal history.
102    pub member: LiveMember<F>,
103    /// Exact new authoritative binding.
104    pub binding_state: BindingState,
105    /// Fix 1 detach cell after terminalization/preservation.
106    pub detach_cell: DetachCell<V>,
107    /// Exact committed phase-2 `Attached` lifecycle record.
108    pub attached: AttachedLifecycleRecord,
109    /// Exact wire success payload.
110    pub outcome: AttachBound,
111    /// Typed old-binding or fenced-recovery terminal effect.
112    pub transition: AttachTransition,
113    binding_origin: BindingOrigin,
114    ordinary_binding_authority: Option<OrdinaryBindingAuthority>,
115    recovered_binding_authority: Option<FencedAttachCommit>,
116}
117
118/// Operational attach state separated from occurrence authority exactly once.
119#[derive(Debug, PartialEq, Eq)]
120pub struct InstalledAttachState<F, V> {
121    /// Rotated membership state.
122    pub member: LiveMember<F>,
123    /// Newly authoritative binding state.
124    pub binding_state: BindingState,
125    /// Detach-cell state after attach.
126    pub detach_cell: DetachCell<V>,
127    /// Committed Attached lifecycle record.
128    pub attached: AttachedLifecycleRecord,
129    /// Wire success payload.
130    pub outcome: AttachBound,
131    /// Typed terminal/attach transition audit.
132    pub transition: AttachTransition,
133    binding_origin: BindingOrigin,
134}
135
136impl<F, V> InstalledAttachState<F, V> {
137    /// Borrows the installed origin through the capsule's canonical writer.
138    #[doc(hidden)]
139    #[must_use]
140    pub fn encode_binding_origin_source(&self) -> alloc::vec::Vec<u8> {
141        super::checkpoint_codec::encode_binding_origin_source(self.binding_origin)
142    }
143}
144
145/// One move-only binding-fate authority emitted by an attach commit.
146#[derive(Debug, PartialEq, Eq)]
147pub struct SealedBindingFateToken {
148    pub(in crate::lifecycle) ordinary: Option<OrdinaryBindingAuthority>,
149    pub(in crate::lifecycle) recovered: Option<FencedAttachCommit>,
150    pub(in crate::lifecycle) cursor: DeliverySeq,
151}
152
153impl<F, V> AttachCommit<F, V> {
154    /// Consumes this commit into operational state and exactly one fate token.
155    #[must_use]
156    pub fn into_slot_and_fate(self) -> (InstalledAttachState<F, V>, SealedBindingFateToken) {
157        let Self {
158            member,
159            binding_state,
160            detach_cell,
161            attached,
162            outcome,
163            transition,
164            binding_origin,
165            ordinary_binding_authority,
166            recovered_binding_authority,
167        } = self;
168        let cursor = member.cursor();
169        (
170            InstalledAttachState {
171                member,
172                binding_state,
173                detach_cell,
174                attached,
175                outcome,
176                transition,
177                binding_origin,
178            },
179            SealedBindingFateToken {
180                ordinary: ordinary_binding_authority,
181                recovered: recovered_binding_authority,
182                cursor,
183            },
184        )
185    }
186
187    /// Projects the binding-ending terminal committed by this attach, if any.
188    #[must_use]
189    pub fn observer_progress_projection(&self) -> Option<ObserverProgressProjection> {
190        let terminal = match self.transition {
191            AttachTransition::Detached
192            | AttachTransition::FencedRecovery {
193                composed_terminal: None,
194                ..
195            } => return None,
196            AttachTransition::Superseded { terminal } => terminal.into(),
197            AttachTransition::FencedRecovery {
198                composed_terminal: Some(terminal),
199                ..
200            } => terminal,
201        };
202        Some(ObserverProgressProjection::sourced(
203            terminal.conversation_id(),
204            terminal.delivery_seq(),
205            super::checkpoint_codec::ObserverProjectionSource::Terminal(
206                terminal.storage_description(),
207            ),
208        ))
209    }
210
211    /// Consumes one exact normal-ack event into this ordinary attach's cursor authority.
212    ///
213    /// Fenced recovery has no ordinary authority and therefore returns the
214    /// unchanged commit. The raw authority never crosses the public boundary.
215    ///
216    /// # Errors
217    ///
218    /// Returns the unchanged commit for fenced recovery or when the event names
219    /// another participant, epoch, previous cursor, or event class.
220    pub fn ordinary_cursor_progressed(mut self, event: Event) -> Result<Self, Box<Self>> {
221        let Some(authority) = self.ordinary_binding_authority.take() else {
222            return Err(Box::new(self));
223        };
224        match authority.cursor_progressed(event) {
225            Ok(authority) => {
226                self.ordinary_binding_authority = Some(authority);
227                Ok(self)
228            }
229            Err(authority) => {
230                self.ordinary_binding_authority = Some(authority);
231                Err(Box::new(self))
232            }
233        }
234    }
235
236    /// Consumes this ordinary attach and its exact durable death into cursor-release fate.
237    ///
238    /// A fenced attach cannot enter this path because it carries no ordinary
239    /// binding authority; recovered epochs still require the distinct
240    /// [`FencedAttachCommit`] provenance transition.
241    ///
242    /// # Errors
243    ///
244    /// Returns the unchanged commit for fenced recovery or unless the terminal
245    /// names this exact ordinary participant, conversation, and binding epoch.
246    pub fn ordinary_binding_fate(
247        mut self,
248        terminal: CommittedDiedTerminal,
249        resulting_floor: DeliverySeq,
250    ) -> Result<OrdinaryBindingFate, Box<Self>> {
251        let Some(authority) = self.ordinary_binding_authority.take() else {
252            return Err(Box::new(self));
253        };
254        match authority.binding_fate(terminal, resulting_floor) {
255            Ok(fate) => Ok(fate),
256            Err(authority) => {
257                self.ordinary_binding_authority = Some(authority);
258                Err(Box::new(self))
259            }
260        }
261    }
262
263    /// Returns no-marker fate authority only for a committed ordinary attach.
264    ///
265    /// Fenced recovery returns `None`; its recovered epoch must instead use the
266    /// [`FencedAttachCommit`] proof that authorized that commit.
267    #[must_use]
268    #[allow(
269        dead_code,
270        reason = "the crate-owned binding-fate operation consumes this sealed attach authority"
271    )]
272    pub(crate) const fn ordinary_binding_authority(&self) -> Option<OrdinaryBindingAuthority> {
273        self.ordinary_binding_authority
274    }
275
276    /// Returns the opaque binding origin emitted from the verified attach mode.
277    #[must_use]
278    #[allow(
279        dead_code,
280        reason = "the crate-owned event replay boundary persists this producer-emitted origin"
281    )]
282    pub(crate) const fn binding_origin(&self) -> BindingOrigin {
283        self.binding_origin
284    }
285}
286
287#[derive(Debug)]
288enum AttachMode {
289    Detached(OrdinaryDetachedAttachAdmission),
290    Superseded(CommittedDetachedTerminal),
291    Fenced {
292        proof: FencedAttachCommit,
293        pending: Option<(PendingFinalization, DeliverySeq)>,
294    },
295}
296
297/// Successful attach proof; fields are private so only mode verification mints it.
298#[derive(Debug)]
299pub struct VerifiedAttachCommit<F> {
300    member: LiveMember<F>,
301    request: CredentialAttachRequest,
302    parameters: AttachCommitParameters,
303    mode: AttachMode,
304}
305
306/// Fenced verification refusal that preserves both consumed authorities.
307#[derive(Debug)]
308pub struct FencedAttachVerificationRefusal<F> {
309    member: LiveMember<F>,
310    proof: FencedAttachCommit,
311    error: AttachVerificationError,
312}
313
314impl<F> FencedAttachVerificationRefusal<F> {
315    /// Returns the typed verification error.
316    #[must_use]
317    pub const fn error(&self) -> AttachVerificationError {
318        self.error
319    }
320
321    /// Returns the unchanged member and proof for same-lock serial retry.
322    #[must_use]
323    pub fn into_parts(self) -> (LiveMember<F>, FencedAttachCommit) {
324        (self.member, self.proof)
325    }
326}
327
328impl<F> LiveMember<F> {
329    /// Verifies an ordinary attach from an already detached member.
330    ///
331    /// # Errors
332    ///
333    /// Returns [`AttachVerificationError`] for authority, marker, state, or allocation mismatch.
334    pub fn verify_detached_attach(
335        self,
336        binding_state: BindingState,
337        closure_admission: OrdinaryDetachedAttachAdmission,
338        request: CredentialAttachRequest,
339        secret_proof: AttachSecretProof,
340        parameters: AttachCommitParameters,
341    ) -> Result<VerifiedAttachCommit<F>, AttachVerificationError> {
342        self.verify_attach_common(&request, secret_proof, &parameters)?;
343        if binding_state != BindingState::Detached {
344            return Err(AttachVerificationError::BindingState);
345        }
346        if request.accept_marker_delivery_seq.is_some() {
347            return Err(AttachVerificationError::MarkerProof);
348        }
349        Ok(VerifiedAttachCommit {
350            member: self,
351            request,
352            parameters,
353            mode: AttachMode::Detached(closure_admission),
354        })
355    }
356
357    /// Verifies an attach that atomically supersedes this member's active epoch.
358    ///
359    /// # Errors
360    ///
361    /// Returns [`AttachVerificationError`] when authority, marker absence,
362    /// handoff ordering, or result allocation is inconsistent.
363    pub fn verify_superseding_attach(
364        self,
365        active_binding: ActiveBinding,
366        request: CredentialAttachRequest,
367        secret_proof: AttachSecretProof,
368        terminal_position: CommittedBindingTerminalPosition,
369        parameters: AttachCommitParameters,
370    ) -> Result<VerifiedAttachCommit<F>, AttachVerificationError> {
371        self.verify_attach_common(&request, secret_proof, &parameters)?;
372        if active_binding.conversation_id != self.conversation_id()
373            || active_binding.participant_id != self.participant_id()
374            || active_binding.binding_epoch.capability_generation != self.generation()
375        {
376            return Err(AttachVerificationError::BindingState);
377        }
378        if request.accept_marker_delivery_seq.is_some() {
379            return Err(AttachVerificationError::MarkerProof);
380        }
381        if terminal_position.transaction_order() != parameters.attached_position.transaction_order()
382        {
383            return Err(AttachVerificationError::LifecycleOrder);
384        }
385        let terminal = active_binding.superseded(terminal_position);
386        Ok(VerifiedAttachCommit {
387            member: self,
388            request,
389            parameters,
390            mode: AttachMode::Superseded(terminal),
391        })
392    }
393
394    /// Verifies marker-fenced recovery proven by the closure-edge transition.
395    ///
396    /// # Errors
397    ///
398    /// Returns [`AttachVerificationError`] when credential authority,
399    /// marker/epoch proof, pending-terminal shape, or result allocation differs.
400    pub fn verify_fenced_attach(
401        self,
402        binding_state: BindingState,
403        request: CredentialAttachRequest,
404        secret_proof: AttachSecretProof,
405        proof: FencedAttachCommit,
406        pending_terminal_delivery_seq: Option<DeliverySeq>,
407        parameters: AttachCommitParameters,
408    ) -> Result<VerifiedAttachCommit<F>, Box<FencedAttachVerificationRefusal<F>>> {
409        let pending = match self.validate_fenced_attach(
410            binding_state,
411            &request,
412            secret_proof,
413            &proof,
414            pending_terminal_delivery_seq,
415            &parameters,
416        ) {
417            Ok(pending) => pending,
418            Err(error) => {
419                return Err(Box::new(FencedAttachVerificationRefusal {
420                    member: self,
421                    proof,
422                    error,
423                }));
424            }
425        };
426        Ok(VerifiedAttachCommit {
427            member: self,
428            request,
429            parameters,
430            mode: AttachMode::Fenced { proof, pending },
431        })
432    }
433
434    fn validate_fenced_attach(
435        &self,
436        binding_state: BindingState,
437        request: &CredentialAttachRequest,
438        secret_proof: AttachSecretProof,
439        proof: &FencedAttachCommit,
440        pending_terminal_delivery_seq: Option<DeliverySeq>,
441        parameters: &AttachCommitParameters,
442    ) -> Result<Option<(PendingFinalization, DeliverySeq)>, AttachVerificationError> {
443        self.verify_attach_common(request, secret_proof, parameters)?;
444        if request.accept_marker_delivery_seq != Some(proof.marker_delivery_seq()) {
445            return Err(AttachVerificationError::MarkerProof);
446        }
447        if proof.participant_id() != self.participant_id()
448            || proof.prior_binding_epoch().capability_generation != self.generation()
449            || proof.new_binding_epoch() != parameters.binding.binding_epoch
450        {
451            return Err(AttachVerificationError::RecoveryAuthority);
452        }
453        match (binding_state, pending_terminal_delivery_seq) {
454            (BindingState::Detached, None) => {
455                if self
456                    .latest_terminal()
457                    .is_none_or(|terminal| terminal.binding_epoch() != proof.prior_binding_epoch())
458                {
459                    return Err(AttachVerificationError::TerminalHistory);
460                }
461                Ok(None)
462            }
463            (BindingState::PendingFinalization(finalization), Some(sequence)) => {
464                let same_conversation = finalization.conversation_id() == self.conversation_id();
465                let same_participant = finalization.participant_id() == self.participant_id();
466                let same_prior_epoch = finalization.binding_epoch() == proof.prior_binding_epoch();
467                if !(same_conversation && same_participant && same_prior_epoch) {
468                    return Err(AttachVerificationError::BindingState);
469                }
470                Ok(Some((finalization, sequence)))
471            }
472            (BindingState::PendingFinalization(_), None) | (BindingState::Detached, Some(_)) => {
473                Err(AttachVerificationError::PendingTerminalSequence)
474            }
475            (BindingState::Bound(_), _) => Err(AttachVerificationError::BindingState),
476        }
477    }
478
479    fn verify_attach_common(
480        &self,
481        request: &CredentialAttachRequest,
482        secret_proof: AttachSecretProof,
483        parameters: &AttachCommitParameters,
484    ) -> Result<(), AttachVerificationError> {
485        if request.conversation_id != self.conversation_id() {
486            return Err(AttachVerificationError::Conversation);
487        }
488        if request.participant_id != self.participant_id() {
489            return Err(AttachVerificationError::Participant);
490        }
491        if request.capability_generation != self.generation() {
492            return Err(AttachVerificationError::Generation);
493        }
494        if secret_proof == AttachSecretProof::Mismatch {
495            return Err(AttachVerificationError::Secret);
496        }
497        if parameters.binding.conversation_id != self.conversation_id()
498            || parameters.binding.participant_id != self.participant_id()
499        {
500            return Err(AttachVerificationError::ResultBinding);
501        }
502        let Some(next_raw) = self.generation().get().checked_add(1) else {
503            return Err(AttachVerificationError::ResultGeneration);
504        };
505        let Some(next_generation) = Generation::new(next_raw) else {
506            return Err(AttachVerificationError::ResultGeneration);
507        };
508        if parameters.binding.binding_epoch.capability_generation != next_generation {
509            return Err(AttachVerificationError::ResultGeneration);
510        }
511        Ok(())
512    }
513}
514
515/// Atomically commits a verified attach and applies Fix 1's detach-cell rule.
516///
517/// # Errors
518///
519/// Returns [`AttachCommitError`] for cell/history mismatch, invalid rotation,
520/// or a canonical receipt invariant rejected after verification.
521pub fn commit_attach<F, V>(
522    verified: VerifiedAttachCommit<F>,
523    detach_cell: DetachCell<V>,
524) -> Result<AttachCommit<F, V>, AttachCommitError>
525where
526    V: Copy + Eq,
527{
528    let VerifiedAttachCommit {
529        member: previous_member,
530        request,
531        parameters,
532        mode,
533    } = verified;
534    let next_cell = transition_detach_cell(&mode, &previous_member, detach_cell)?;
535    let attached =
536        AttachedLifecycleRecord::from_binding(parameters.binding, parameters.attached_position);
537    let (persisted_cursor, terminal, transition, binding_origin, recovered_binding_authority) =
538        match mode {
539            AttachMode::Detached(_) => (
540                previous_member.cursor(),
541                None,
542                AttachTransition::Detached,
543                BindingOrigin::unfenced(attached),
544                None,
545            ),
546            AttachMode::Superseded(committed) => (
547                previous_member.cursor(),
548                Some(CommittedBindingTerminal::from(committed)),
549                AttachTransition::Superseded {
550                    terminal: committed,
551                },
552                BindingOrigin::unfenced(attached),
553                None,
554            ),
555            AttachMode::Fenced { proof, pending } => {
556                let composed_terminal =
557                    pending.map(|(finalization, sequence)| finalization.commit(sequence));
558                let marker_delivery_seq = proof.marker_delivery_seq();
559                let prior_binding_epoch = proof.prior_binding_epoch();
560                let next_closure_state = proof.next_state();
561                // The recovery marker says where DELIVERY stands; it does not
562                // say what this participant has acknowledged. Where the
563                // participant is already past the marker, its own cumulative
564                // boundary is the durable fact and the marker is behind it —
565                // so the cursor carried across the generation is the later of
566                // the two, never the marker alone. Taking the marker alone
567                // moved the cursor BACKWARDS on a fenced recovery, and a
568                // participant's next cumulative acknowledgement was then
569                // refused as a gap over records it had already acknowledged.
570                // The provenance below still records the marker's own
571                // sequence, which is what the proof carried.
572                let persisted_cursor = previous_member.cursor().max(marker_delivery_seq);
573                (
574                    persisted_cursor,
575                    composed_terminal,
576                    AttachTransition::FencedRecovery {
577                        prior_binding_epoch,
578                        composed_terminal,
579                        next_closure_state,
580                    },
581                    BindingOrigin::recovered(attached, marker_delivery_seq, prior_binding_epoch),
582                    Some(proof),
583                )
584            }
585        };
586    let result_generation = parameters.binding.binding_epoch.capability_generation;
587    let member = previous_member
588        .rotate(
589            result_generation,
590            parameters.attach_secret,
591            persisted_cursor,
592            terminal,
593        )
594        .map_err(AttachCommitError::MembershipInvariant)?;
595    let outcome = match transition {
596        AttachTransition::FencedRecovery { .. } => AttachBound::fenced(
597            request.conversation_id,
598            request.attach_attempt_token,
599            request.participant_id,
600            request.capability_generation,
601            parameters.attach_secret,
602            parameters.binding.binding_epoch,
603            persisted_cursor,
604            parameters.receipt_expires_at,
605            parameters.provenance_expires_at,
606        ),
607        AttachTransition::Detached | AttachTransition::Superseded { .. } => AttachBound::ordinary(
608            request.conversation_id,
609            request.attach_attempt_token,
610            request.participant_id,
611            request.capability_generation,
612            parameters.attach_secret,
613            parameters.binding.binding_epoch,
614            persisted_cursor,
615            parameters.receipt_expires_at,
616            parameters.provenance_expires_at,
617        ),
618    }
619    .ok_or(AttachCommitError::ReceiptInvariant)?;
620    let ordinary_binding_authority = match transition {
621        AttachTransition::Detached | AttachTransition::Superseded { .. } => Some(
622            OrdinaryBindingAuthority::new(parameters.binding, persisted_cursor),
623        ),
624        AttachTransition::FencedRecovery { .. } => None,
625    };
626    Ok(AttachCommit {
627        member,
628        binding_state: BindingState::Bound(parameters.binding),
629        detach_cell: next_cell,
630        attached,
631        outcome,
632        transition,
633        binding_origin,
634        ordinary_binding_authority,
635        recovered_binding_authority,
636    })
637}
638
639fn transition_detach_cell<F, V>(
640    mode: &AttachMode,
641    member: &LiveMember<F>,
642    detach_cell: DetachCell<V>,
643) -> Result<DetachCell<V>, AttachCommitError>
644where
645    V: Copy + Eq,
646{
647    match detach_cell {
648        DetachCell::Empty(cell) => Ok(DetachCell::Empty(cell)),
649        DetachCell::Pending(cell) => {
650            let AttachMode::Fenced {
651                pending: Some((binding_state, _)),
652                ..
653            } = mode
654            else {
655                return Err(AttachCommitError::PendingDetach);
656            };
657            validate_pending_pair(
658                BindingState::PendingFinalization(*binding_state),
659                &cell,
660                Some(member.conversation_id()),
661            )
662            .map_err(|_| AttachCommitError::BindingCellState)?;
663            Ok(DetachCell::Terminalized(cell.terminalize_after_attach()))
664        }
665        DetachCell::Committed(cell) => {
666            if matches!(mode, AttachMode::Superseded(_))
667                || cell.participant_id() != member.participant_id()
668                || cell.request_generation() != member.generation()
669            {
670                return Err(AttachCommitError::DetachCellAuthority);
671            }
672            let Some(terminal) = member.latest_terminal() else {
673                return Err(AttachCommitError::TerminalHistory);
674            };
675            if terminal.detached_cause() != Some(crate::wire::DetachedCause::CleanDeregister)
676                || terminal.binding_epoch() != cell.committed_binding_epoch()
677                || terminal.delivery_seq() != cell.detached_delivery_seq()
678            {
679                return Err(AttachCommitError::TerminalHistory);
680            }
681            Ok(DetachCell::Terminalized(cell.terminalize_after_attach()))
682        }
683        DetachCell::Terminalized(cell) => {
684            if cell.participant_id() != member.participant_id() {
685                return Err(AttachCommitError::DetachCellAuthority);
686            }
687            Ok(DetachCell::Terminalized(cell))
688        }
689    }
690}
691
692impl SealedBindingFateToken {
693    /// Borrows the closed fate class, exact proof fields, and cursor into bytes.
694    ///
695    /// # Errors
696    ///
697    /// Refuses an absent or ambiguous fate class and an inconsistent cursor.
698    pub fn encode_checkpoint(
699        &self,
700    ) -> Result<alloc::vec::Vec<u8>, super::OwnerCheckpointRestoreError> {
701        super::checkpoint_codec::encode_fate_token(self)
702    }
703
704    /// Restores against an already validated origin and optional fenced proof.
705    ///
706    /// A recovered proof is moved once; raw bytes never manufacture its authority.
707    ///
708    /// # Errors
709    ///
710    /// Refuses framing, a mismatched origin or cursor, missing recovered provenance,
711    /// or any byte difference from the validated fenced proof.
712    pub fn restore_checkpoint(
713        body: &[u8],
714        origin: &BindingOrigin,
715        cursor: DeliverySeq,
716        recovered: Option<FencedAttachCommit>,
717    ) -> Result<Self, super::OwnerCheckpointRestoreError> {
718        super::checkpoint_codec::restore_fate_token(body, origin, cursor, recovered)
719    }
720}