Skip to main content

liminal_protocol/lifecycle/
lookup.rs

1use crate::wire::{
2    AttachBound, AttachEnvelope, AttemptConflict, AttemptTokenBodyConflict, BindingEpoch,
3    BindingRequiredEnvelope, BindingStateView, CommonStaleAuthorityEnvelope,
4    CredentialAttachRequest, DeliverySeq, DetachCommitted, DetachEnvelope, DetachInProgress,
5    DetachRequest, DetachStaleAuthority, EnrollBound, EnrollmentEnvelope, EnrollmentKnown,
6    EnrollmentRequest, Generation, LeaveCommitted, LeaveEnvelope, LeaveRequest,
7    LeaveStaleAuthority, MarkerAck, MarkerAckEnvelope, NoBinding, ParticipantAck,
8    ParticipantAckEnvelope, ParticipantReferenceEnvelope, ParticipantUnknown, ReceiptExpired,
9    ReceiptExpiryReason, ReceiptReplay, RecordAdmission, RecordAdmissionEnvelope, Retired,
10    StaleAuthority, StaleOrUnknownReceipt,
11};
12
13use super::{
14    ActiveBinding, BindingState, DetachCell, IdentityState, LiveMember, PendingReplayRequest,
15    RetiredIdentity,
16};
17
18/// Result of resolving the presented participant key before operation-specific
19/// authority checks.
20#[derive(Debug)]
21pub enum PresentedIdentity<'a, EF, V, LF> {
22    /// No live identity or tombstone exists for the presented key.
23    Absent,
24    /// The presented key resolves to live membership.
25    Live(&'a LiveMember<EF>),
26    /// The presented key resolves to a permanent tombstone.
27    Retired(&'a RetiredIdentity<EF, V, LF>),
28}
29
30impl<EF, V, LF> Copy for PresentedIdentity<'_, EF, V, LF> {}
31
32impl<EF, V, LF> Clone for PresentedIdentity<'_, EF, V, LF> {
33    fn clone(&self) -> Self {
34        *self
35    }
36}
37
38impl<'a, EF, V, LF> From<Option<&'a IdentityState<EF, V, LF>>>
39    for PresentedIdentity<'a, EF, V, LF>
40{
41    fn from(value: Option<&'a IdentityState<EF, V, LF>>) -> Self {
42        match value {
43            None => Self::Absent,
44            Some(IdentityState::Live(member)) => Self::Live(member),
45            Some(IdentityState::Retired(tombstone)) => Self::Retired(tombstone),
46        }
47    }
48}
49
50/// Identity reached through an exact token index before the presented-key
51/// identity lookup runs.
52///
53/// Absence is represented by the operation-specific token phase. A resolved
54/// token always names either a live identity or its permanent tombstone.
55#[derive(Debug)]
56pub enum ResolvedIdentity<'a, EF, V, LF> {
57    /// The token index resolves to live membership.
58    Live(&'a LiveMember<EF>),
59    /// The token index resolves to a permanent tombstone.
60    Retired(&'a RetiredIdentity<EF, V, LF>),
61}
62
63impl<EF, V, LF> Copy for ResolvedIdentity<'_, EF, V, LF> {}
64
65impl<EF, V, LF> Clone for ResolvedIdentity<'_, EF, V, LF> {
66    fn clone(&self) -> Self {
67        *self
68    }
69}
70
71impl<'a, EF, V, LF> From<&'a IdentityState<EF, V, LF>> for ResolvedIdentity<'a, EF, V, LF> {
72    fn from(value: &'a IdentityState<EF, V, LF>) -> Self {
73        match value {
74            IdentityState::Live(member) => Self::Live(member),
75            IdentityState::Retired(tombstone) => Self::Retired(tombstone),
76        }
77    }
78}
79
80/// Secret-verifier result supplied by the consuming cryptographic layer for
81/// credential attach lookup.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum AttachSecretProof {
84    /// The constant-time verifier rejected the presented secret.
85    Mismatch,
86    /// The constant-time verifier accepted the presented secret.
87    Verified,
88}
89
90/// Stored secret-bearing enrollment receipt while its receipt deadline is
91/// live.
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct EnrollmentLiveReceipt {
94    committed: EnrollBound,
95}
96
97impl EnrollmentLiveReceipt {
98    /// Captures the exact committed enrollment payload as a live receipt.
99    #[must_use]
100    pub const fn from_commit(committed: EnrollBound) -> Self {
101        Self { committed }
102    }
103
104    /// Borrows the complete immutable committed receipt payload.
105    #[must_use]
106    pub const fn committed(&self) -> &EnrollBound {
107        &self.committed
108    }
109
110    /// Moves the complete committed payload without copying receipt state.
111    #[must_use]
112    pub const fn into_committed(self) -> EnrollBound {
113        self.committed
114    }
115
116    /// Encodes the exact committed v1 wire body by borrowing its payload.
117    #[must_use]
118    pub fn encode_committed_body(&self) -> alloc::vec::Vec<u8> {
119        crate::wire::encode_enrollment_committed_body(&self.committed)
120    }
121
122    /// Decodes one exact committed v1 wire body, moving the validated payload.
123    ///
124    /// # Errors
125    ///
126    /// Returns [`crate::wire::CodecError`] for a noncanonical field, wrong
127    /// originating request, incomplete body or trailing bytes.
128    pub fn decode_committed_body(bytes: &[u8]) -> Result<Self, crate::wire::CodecError> {
129        let (value, _) = crate::wire::decode_server_value_body(
130            crate::wire::ServerDiscriminant::EnrollBound,
131            crate::wire::ProtocolVersion::V1,
132            bytes,
133        )?;
134        let crate::wire::ServerValue::EnrollBound(committed) = value else {
135            return Err(crate::wire::CodecError::InvalidValue);
136        };
137        Ok(Self::from_commit(committed))
138    }
139}
140
141/// Non-secret enrollment provenance retained after the live receipt is
142/// deleted.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct EnrollmentProvenance {
145    result_generation: Generation,
146    reason: ReceiptExpiryReason,
147}
148
149impl EnrollmentProvenance {
150    /// Creates the exact-reason enrollment provenance row.
151    #[must_use]
152    pub const fn new(result_generation: Generation, reason: ReceiptExpiryReason) -> Self {
153        Self {
154            result_generation,
155            reason,
156        }
157    }
158}
159
160/// Phase-specific enrollment-token lookup state after the lifetime token index
161/// has run.
162#[derive(Debug)]
163pub enum EnrollmentTokenPhase<'a, EF, V, LF> {
164    /// No lifetime mapping exists; enrollment may run its remaining checks.
165    Unmapped,
166    /// The exact secret-bearing receipt remains live.
167    LiveReceipt {
168        /// Participant resolved by the enrollment token.
169        identity: ResolvedIdentity<'a, EF, V, LF>,
170        /// Stored canonical commit payload.
171        receipt: &'a EnrollmentLiveReceipt,
172    },
173    /// Only exact-reason non-secret provenance remains.
174    Provenance {
175        /// Participant resolved by the enrollment token.
176        identity: ResolvedIdentity<'a, EF, V, LF>,
177        /// Stored provenance fields.
178        provenance: EnrollmentProvenance,
179    },
180    /// The permanent lifetime mapping remains after receipt provenance.
181    LifetimeMapping {
182        /// Participant resolved by the enrollment token.
183        identity: ResolvedIdentity<'a, EF, V, LF>,
184    },
185}
186
187impl<EF, V, LF> Copy for EnrollmentTokenPhase<'_, EF, V, LF> {}
188
189impl<EF, V, LF> Clone for EnrollmentTokenPhase<'_, EF, V, LF> {
190    fn clone(&self) -> Self {
191        *self
192    }
193}
194
195/// Total enrollment-token lookup result through phase 0c.
196#[derive(Clone, Debug, PartialEq, Eq)]
197pub enum EnrollmentLookupResult {
198    /// A token mapping resolved to a tombstone before any live token result.
199    Retired(Retired),
200    /// An exact live receipt still names its origin binding.
201    Bound(ReceiptReplay),
202    /// An exact live receipt no longer names its origin binding.
203    UnboundReceipt(ReceiptReplay),
204    /// Exact non-secret provenance remains.
205    ReceiptExpired(ReceiptExpired),
206    /// Only the permanent live enrollment mapping remains.
207    EnrollmentKnown(EnrollmentKnown),
208    /// No token mapping exists; fresh enrollment may continue.
209    AuthorizedNew,
210}
211
212/// Applies token-to-identity tombstone precedence and enrollment's three live
213/// token phases.
214#[must_use]
215pub fn lookup_enrollment<EF, V, LF>(
216    phase: EnrollmentTokenPhase<'_, EF, V, LF>,
217    binding: &BindingState,
218    request: &EnrollmentRequest,
219) -> EnrollmentLookupResult {
220    match phase {
221        EnrollmentTokenPhase::Unmapped => EnrollmentLookupResult::AuthorizedNew,
222        EnrollmentTokenPhase::LiveReceipt { identity, receipt } => match identity {
223            ResolvedIdentity::Retired(tombstone) => {
224                EnrollmentLookupResult::Retired(retired_enrollment(tombstone, request))
225            }
226            ResolvedIdentity::Live(member) => {
227                let committed = &receipt.committed;
228                let replay = ReceiptReplay::Enrollment(committed.clone());
229                if receipt_binding_is_current(
230                    binding,
231                    request.conversation_id,
232                    member.participant_id(),
233                    committed.origin_binding_epoch(),
234                ) {
235                    EnrollmentLookupResult::Bound(replay)
236                } else {
237                    EnrollmentLookupResult::UnboundReceipt(replay)
238                }
239            }
240        },
241        EnrollmentTokenPhase::Provenance {
242            identity,
243            provenance,
244        } => match identity {
245            ResolvedIdentity::Retired(tombstone) => {
246                EnrollmentLookupResult::Retired(retired_enrollment(tombstone, request))
247            }
248            ResolvedIdentity::Live(member) => {
249                EnrollmentLookupResult::ReceiptExpired(ReceiptExpired::Enrollment {
250                    conversation_id: request.conversation_id,
251                    token: request.enrollment_token,
252                    participant_id: member.participant_id(),
253                    result_generation: provenance.result_generation,
254                    current_generation: member.generation(),
255                    reason: provenance.reason,
256                })
257            }
258        },
259        EnrollmentTokenPhase::LifetimeMapping { identity } => match identity {
260            ResolvedIdentity::Retired(tombstone) => {
261                EnrollmentLookupResult::Retired(retired_enrollment(tombstone, request))
262            }
263            ResolvedIdentity::Live(member) => {
264                EnrollmentLookupResult::EnrollmentKnown(EnrollmentKnown {
265                    conversation_id: request.conversation_id,
266                    token: request.enrollment_token,
267                    participant_id: member.participant_id(),
268                    current_generation: member.generation(),
269                })
270            }
271        },
272    }
273}
274
275/// Stored canonical credential-attach result while its secret-bearing receipt
276/// remains live.
277#[derive(Clone, Debug, PartialEq, Eq)]
278pub struct CredentialAttachLiveReceipt {
279    committed: AttachBound,
280}
281
282impl CredentialAttachLiveReceipt {
283    /// Captures the exact committed attach payload as a live receipt.
284    #[must_use]
285    pub const fn from_commit(committed: AttachBound) -> Self {
286        Self { committed }
287    }
288
289    /// Borrows the complete immutable committed receipt payload.
290    #[must_use]
291    pub const fn committed(&self) -> &AttachBound {
292        &self.committed
293    }
294
295    /// Moves the complete committed payload without copying receipt state.
296    #[must_use]
297    pub const fn into_committed(self) -> AttachBound {
298        self.committed
299    }
300
301    /// Encodes the exact committed v1 wire body by borrowing its payload.
302    #[must_use]
303    pub fn encode_committed_body(&self) -> alloc::vec::Vec<u8> {
304        crate::wire::encode_attach_committed_body(&self.committed)
305    }
306
307    /// Decodes one exact committed v1 wire body, moving the validated payload.
308    ///
309    /// # Errors
310    ///
311    /// Returns [`crate::wire::CodecError`] for a noncanonical field, wrong
312    /// originating request, incomplete body or trailing bytes.
313    pub fn decode_committed_body(bytes: &[u8]) -> Result<Self, crate::wire::CodecError> {
314        let (value, _) = crate::wire::decode_server_value_body(
315            crate::wire::ServerDiscriminant::AttachBound,
316            crate::wire::ProtocolVersion::V1,
317            bytes,
318        )?;
319        let crate::wire::ServerValue::AttachBound(committed) = value else {
320            return Err(crate::wire::CodecError::InvalidValue);
321        };
322        Ok(Self::from_commit(committed))
323    }
324}
325
326/// Non-secret credential-attach provenance retained after deleting the live
327/// verifier and secret body.
328#[derive(Clone, Copy, Debug, PartialEq, Eq)]
329pub struct CredentialAttachProvenance {
330    result_generation: Generation,
331    reason: ReceiptExpiryReason,
332}
333
334impl CredentialAttachProvenance {
335    /// Creates one exact-reason credential-attach provenance row.
336    #[must_use]
337    pub const fn new(result_generation: Generation, reason: ReceiptExpiryReason) -> Self {
338        Self {
339            result_generation,
340            reason,
341        }
342    }
343}
344
345/// Phase selected by credential-attach token lookup.
346#[derive(Debug)]
347pub enum CredentialAttachTokenPhase<'a, EF, V, LF> {
348    /// No receipt or provenance key matched.
349    NoMatch,
350    /// The exact secret-bearing receipt remains live.
351    LiveReceipt {
352        /// Participant resolved by the exact token key.
353        identity: ResolvedIdentity<'a, EF, V, LF>,
354        /// Stored canonical result and original non-secret request fields.
355        receipt: &'a CredentialAttachLiveReceipt,
356    },
357    /// The exact token key reached non-secret provenance.
358    Provenance {
359        /// Participant resolved by the exact token key.
360        identity: ResolvedIdentity<'a, EF, V, LF>,
361        /// Exact terminal reason and result generation.
362        provenance: CredentialAttachProvenance,
363    },
364    /// Receipt provenance is gone, so exact-old and unknown-old status is
365    /// intentionally ambiguous.
366    AfterProvenance,
367}
368
369impl<EF, V, LF> Copy for CredentialAttachTokenPhase<'_, EF, V, LF> {}
370
371impl<EF, V, LF> Clone for CredentialAttachTokenPhase<'_, EF, V, LF> {
372    fn clone(&self) -> Self {
373        *self
374    }
375}
376
377/// Total credential-attach lookup result through authority phase 3.
378#[derive(Clone, Debug, PartialEq, Eq)]
379pub enum CredentialAttachLookupResult<'a, EF> {
380    /// An exact token or the presented key resolved to a tombstone.
381    Retired(Retired),
382    /// Neither a live identity nor tombstone exists.
383    ParticipantUnknown(ParticipantUnknown),
384    /// Secret or current-generation authority is stale.
385    StaleAuthority(StaleAuthority),
386    /// Verified exact live receipt changed a canonical non-secret field.
387    AttemptTokenBodyConflict(AttemptTokenBodyConflict),
388    /// Exact live receipt still names its origin binding.
389    Bound(ReceiptReplay),
390    /// Exact live receipt no longer names its origin binding.
391    UnboundReceipt(ReceiptReplay),
392    /// Exact non-secret provenance remains.
393    ReceiptExpired(ReceiptExpired),
394    /// No verifier remains after provenance.
395    StaleOrUnknownReceipt(StaleOrUnknownReceipt),
396    /// Fresh live authority passed and attach may run remaining checks.
397    AuthorizedFresh {
398        /// Verified live member; attach itself does not require a binding.
399        member: &'a LiveMember<EF>,
400    },
401}
402
403/// Applies credential-attach token lookup, tombstone precedence, verifier
404/// order, and ordinary live-authority checks.
405///
406/// `secret_proof` is computed against the live receipt verifier when the phase
407/// is [`CredentialAttachTokenPhase::LiveReceipt`], and against the current live
408/// member for [`CredentialAttachTokenPhase::NoMatch`]. Provenance phases have no
409/// verifier and ignore it by construction of their result path.
410#[must_use]
411pub fn lookup_credential_attach<'a, EF, V, LF>(
412    token_phase: CredentialAttachTokenPhase<'a, EF, V, LF>,
413    presented_identity: PresentedIdentity<'a, EF, V, LF>,
414    binding: &BindingState,
415    request: &CredentialAttachRequest,
416    secret_proof: AttachSecretProof,
417) -> CredentialAttachLookupResult<'a, EF> {
418    match token_phase {
419        CredentialAttachTokenPhase::LiveReceipt { identity, receipt } => {
420            return match identity {
421                ResolvedIdentity::Retired(tombstone) => {
422                    CredentialAttachLookupResult::Retired(retired_attach(tombstone, request))
423                }
424                ResolvedIdentity::Live(member) => {
425                    lookup_live_attach_receipt(member, binding, request, secret_proof, receipt)
426                }
427            };
428        }
429        CredentialAttachTokenPhase::Provenance {
430            identity,
431            provenance,
432        } => {
433            return match identity {
434                ResolvedIdentity::Retired(tombstone) => {
435                    CredentialAttachLookupResult::Retired(retired_attach(tombstone, request))
436                }
437                ResolvedIdentity::Live(member) => {
438                    CredentialAttachLookupResult::ReceiptExpired(ReceiptExpired::CredentialAttach {
439                        conversation_id: request.conversation_id,
440                        token: request.attach_attempt_token,
441                        participant_id: request.participant_id,
442                        presented_generation: request.capability_generation,
443                        presented_marker_delivery_seq: request.accept_marker_delivery_seq,
444                        result_generation: provenance.result_generation,
445                        current_generation: member.generation(),
446                        reason: provenance.reason,
447                    })
448                }
449            };
450        }
451        CredentialAttachTokenPhase::NoMatch | CredentialAttachTokenPhase::AfterProvenance => {}
452    }
453
454    let member = match presented_identity {
455        PresentedIdentity::Retired(tombstone) => {
456            return CredentialAttachLookupResult::Retired(retired_attach(tombstone, request));
457        }
458        PresentedIdentity::Absent => {
459            return CredentialAttachLookupResult::ParticipantUnknown(unknown_attach(request));
460        }
461        PresentedIdentity::Live(member) => {
462            if member.conversation_id() != request.conversation_id
463                || member.participant_id() != request.participant_id
464            {
465                return CredentialAttachLookupResult::ParticipantUnknown(unknown_attach(request));
466            }
467            member
468        }
469    };
470
471    if matches!(token_phase, CredentialAttachTokenPhase::AfterProvenance) {
472        return CredentialAttachLookupResult::StaleOrUnknownReceipt(StaleOrUnknownReceipt {
473            conversation_id: request.conversation_id,
474            token: request.attach_attempt_token,
475            participant_id: request.participant_id,
476            presented_generation: request.capability_generation,
477            presented_marker_delivery_seq: request.accept_marker_delivery_seq,
478            current_generation: member.generation(),
479        });
480    }
481
482    if request.capability_generation != member.generation()
483        || secret_proof == AttachSecretProof::Mismatch
484    {
485        return CredentialAttachLookupResult::StaleAuthority(stale_attach(
486            request,
487            member.generation(),
488        ));
489    }
490
491    CredentialAttachLookupResult::AuthorizedFresh { member }
492}
493
494fn lookup_live_attach_receipt<'a, EF>(
495    member: &'a LiveMember<EF>,
496    binding: &BindingState,
497    request: &CredentialAttachRequest,
498    secret_proof: AttachSecretProof,
499    receipt: &CredentialAttachLiveReceipt,
500) -> CredentialAttachLookupResult<'a, EF> {
501    if secret_proof == AttachSecretProof::Mismatch {
502        return CredentialAttachLookupResult::StaleAuthority(stale_attach(
503            request,
504            member.generation(),
505        ));
506    }
507
508    let committed = &receipt.committed;
509    let conflict = if request.capability_generation != committed.request_generation() {
510        Some(AttemptConflict::Generation)
511    } else if request.accept_marker_delivery_seq != committed.accepted_marker_delivery_seq() {
512        Some(AttemptConflict::MarkerDeliverySequence)
513    } else {
514        None
515    };
516    if let Some(conflict) = conflict {
517        return CredentialAttachLookupResult::AttemptTokenBodyConflict(
518            AttemptTokenBodyConflict::CredentialAttach {
519                token: request.attach_attempt_token,
520                conversation_id: request.conversation_id,
521                presented_participant_id: request.participant_id,
522                presented_generation: request.capability_generation,
523                presented_marker_delivery_seq: request.accept_marker_delivery_seq,
524                conflict,
525            },
526        );
527    }
528
529    let replay = ReceiptReplay::CredentialAttach(committed.clone());
530    if receipt_binding_is_current(
531        binding,
532        request.conversation_id,
533        request.participant_id,
534        committed.origin_binding_epoch(),
535    ) {
536        CredentialAttachLookupResult::Bound(replay)
537    } else {
538        CredentialAttachLookupResult::UnboundReceipt(replay)
539    }
540}
541
542/// Binding-required participant request families that share lookup phases
543/// 1 through 5.
544#[derive(Clone, Debug, PartialEq, Eq)]
545pub enum ParticipantBindingRequest {
546    /// Continuous cumulative acknowledgement.
547    ParticipantAck(ParticipantAck),
548    /// Explicit retained-marker acknowledgement.
549    MarkerAck(MarkerAck),
550    /// Ordinary record admission.
551    RecordAdmission(RecordAdmission),
552}
553
554/// Total shared lookup result for ack, marker-ack, and ordinary admission.
555#[derive(Clone, Debug, PartialEq, Eq)]
556pub enum BindingRequiredLookupResult<'a, EF> {
557    /// Presented participant is permanently retired.
558    Retired(Retired),
559    /// Presented participant is unknown.
560    ParticipantUnknown(ParticipantUnknown),
561    /// Presented generation is stale.
562    StaleAuthority(StaleAuthority),
563    /// Exact receiving binding epoch is absent or different.
564    NoBinding(NoBinding),
565    /// Phases 1 through 4 passed and operation-specific checks may run.
566    Authorized {
567        /// Verified live member.
568        member: &'a LiveMember<EF>,
569        /// Exact current authoritative binding.
570        binding: ActiveBinding,
571    },
572}
573
574/// Applies the common tombstone, unknown, generation, and exact-binding order
575/// for participant ack, marker ack, and ordinary admission.
576#[must_use]
577pub fn lookup_binding_required<'a, EF, V, LF>(
578    identity: PresentedIdentity<'a, EF, V, LF>,
579    binding: &BindingState,
580    receiving_binding_epoch: Option<BindingEpoch>,
581    request: &ParticipantBindingRequest,
582) -> BindingRequiredLookupResult<'a, EF> {
583    let member = match identity {
584        PresentedIdentity::Retired(tombstone) => {
585            return BindingRequiredLookupResult::Retired(Retired::Participant {
586                request: participant_reference(request),
587                retired_generation: tombstone.retired_generation(),
588            });
589        }
590        PresentedIdentity::Absent => {
591            return BindingRequiredLookupResult::ParticipantUnknown(ParticipantUnknown {
592                request: participant_reference(request),
593            });
594        }
595        PresentedIdentity::Live(member) => {
596            if member.conversation_id() != binding_request_conversation(request)
597                || member.participant_id() != binding_request_participant(request)
598            {
599                return BindingRequiredLookupResult::ParticipantUnknown(ParticipantUnknown {
600                    request: participant_reference(request),
601                });
602            }
603            member
604        }
605    };
606
607    if binding_request_generation(request) != member.generation() {
608        return BindingRequiredLookupResult::StaleAuthority(StaleAuthority::Live {
609            request: common_stale_envelope(request),
610            current_generation: member.generation(),
611        });
612    }
613
614    match binding {
615        BindingState::Bound(active)
616            if active.conversation_id == member.conversation_id()
617                && active.participant_id == member.participant_id()
618                && active.binding_epoch.capability_generation == member.generation()
619                && receiving_binding_epoch == Some(active.binding_epoch) =>
620        {
621            BindingRequiredLookupResult::Authorized {
622                member,
623                binding: *active,
624            }
625        }
626        BindingState::Detached | BindingState::PendingFinalization(_) | BindingState::Bound(_) => {
627            BindingRequiredLookupResult::NoBinding(NoBinding {
628                request: binding_required_envelope(request),
629            })
630        }
631    }
632}
633
634/// Exact-token resolution state for the identity-slot detach cell.
635#[derive(Debug)]
636pub enum DetachTokenResolution<'a, EF, V, LF> {
637    /// The presented token does not equal the stored cell token.
638    NoExactMatch,
639    /// The exact token index resolved its owning identity.
640    Exact(ResolvedIdentity<'a, EF, V, LF>),
641}
642
643impl<EF, V, LF> Copy for DetachTokenResolution<'_, EF, V, LF> {}
644
645impl<EF, V, LF> Clone for DetachTokenResolution<'_, EF, V, LF> {
646    fn clone(&self) -> Self {
647        *self
648    }
649}
650
651/// Complete serialized context for exhaustive detach reference lookup.
652///
653/// Grouping these values prevents a caller from accidentally omitting the
654/// exact receiving binding epoch while still keeping token-resolved and
655/// presented-key identities distinct.
656pub struct DetachLookupContext<'a, EF, V, LF, D> {
657    /// Exact-token index result selected before presented-key lookup.
658    pub token_resolution: DetachTokenResolution<'a, EF, V, LF>,
659    /// Identity found under the request's presented participant key.
660    pub presented_identity: PresentedIdentity<'a, EF, V, LF>,
661    /// Current four-variant identity-slot detach cell.
662    pub cell: &'a DetachCell<D>,
663    /// Current binding or pending-finalization state.
664    pub binding: &'a BindingState,
665    /// Binding epoch carried by the receiving connection's serialized context.
666    pub receiving_binding_epoch: Option<BindingEpoch>,
667    /// Exact decoded detach request.
668    pub request: &'a DetachRequest,
669    /// Canonical non-secret verifier computed by the consuming server.
670    pub request_verifier: D,
671    /// Observer progress used only to prepare exact pending replay.
672    pub observer_progress: DeliverySeq,
673}
674
675/// Total result of detach participant/token/binding lookup.
676#[derive(Clone, Debug, PartialEq, Eq)]
677pub enum DetachLookupResult<'a, EF, D> {
678    /// A tombstone won before every live detach-cell result.
679    Retired(Retired),
680    /// No participant identity or tombstone exists.
681    ParticipantUnknown(ParticipantUnknown),
682    /// Exact-token or ordinary live authority is stale.
683    StaleAuthority(DetachStaleAuthority),
684    /// Authority is live but this request has no exact current binding.
685    NoBinding(NoBinding),
686    /// An exact pending token requires the equality/drain/rewrite transition.
687    PendingReplayRequired(PendingReplayRequest<D>),
688    /// A different token encountered a pending detach cell.
689    DetachInProgress(DetachInProgress),
690    /// An exact committed token replays the stable detach result.
691    DetachCommitted(DetachCommitted),
692    /// All lookup and binding checks passed; remaining detach checks may run.
693    Authorized {
694        /// Verified live membership.
695        member: &'a LiveMember<EF>,
696        /// Exact current binding authority.
697        binding: ActiveBinding,
698    },
699}
700
701/// Applies the total participant lookup and detach-cell precedence.
702///
703/// Tombstone classification precedes all live cell results. For a live
704/// identity, exact cell replay precedes generation and binding checks; a
705/// different token against `Pending` precedes those checks as
706/// `DetachInProgress`. A different token against `Committed` or `Terminalized`
707/// continues through ordinary authority lookup.
708#[must_use]
709pub fn lookup_detach<'a, EF, V, LF, D>(
710    context: &DetachLookupContext<'a, EF, V, LF, D>,
711) -> DetachLookupResult<'a, EF, D>
712where
713    D: Copy + Eq,
714{
715    let token_resolution = context.token_resolution;
716    let presented_identity = context.presented_identity;
717    let cell = context.cell;
718    let binding = context.binding;
719    let receiving_binding_epoch = context.receiving_binding_epoch;
720    let request = context.request;
721    let request_verifier = context.request_verifier;
722    let observer_progress = context.observer_progress;
723    let exact_member = match token_resolution {
724        DetachTokenResolution::Exact(ResolvedIdentity::Retired(tombstone)) => {
725            return DetachLookupResult::Retired(retired_detach(tombstone, request));
726        }
727        DetachTokenResolution::Exact(ResolvedIdentity::Live(member)) => Some(member),
728        DetachTokenResolution::NoExactMatch => None,
729    };
730
731    if let Some(member) = exact_member
732        && let Some(result) = lookup_exact_detach_cell(
733            member,
734            cell,
735            binding,
736            request,
737            request_verifier,
738            observer_progress,
739        )
740    {
741        return result;
742    }
743
744    let member = match presented_identity {
745        PresentedIdentity::Retired(tombstone) => {
746            return DetachLookupResult::Retired(retired_detach(tombstone, request));
747        }
748        PresentedIdentity::Absent => {
749            return DetachLookupResult::ParticipantUnknown(unknown_detach(request));
750        }
751        PresentedIdentity::Live(member) => {
752            if member.conversation_id() != request.conversation_id
753                || member.participant_id() != request.participant_id
754            {
755                return DetachLookupResult::ParticipantUnknown(unknown_detach(request));
756            }
757            member
758        }
759    };
760
761    if matches!(token_resolution, DetachTokenResolution::NoExactMatch)
762        && let DetachCell::Pending(pending) = cell
763    {
764        return DetachLookupResult::DetachInProgress(pending.competing_attempt(
765            request.conversation_id,
766            request.detach_attempt_token,
767            request.capability_generation,
768        ));
769    }
770
771    if request.capability_generation != member.generation() {
772        return stale_detach(request, member.generation());
773    }
774
775    match binding {
776        BindingState::Bound(active)
777            if active.conversation_id == request.conversation_id
778                && active.participant_id == request.participant_id
779                && active.binding_epoch.capability_generation == request.capability_generation
780                && receiving_binding_epoch == Some(active.binding_epoch) =>
781        {
782            DetachLookupResult::Authorized {
783                member,
784                binding: *active,
785            }
786        }
787        BindingState::Detached | BindingState::PendingFinalization(_) | BindingState::Bound(_) => {
788            DetachLookupResult::NoBinding(no_binding_detach(request))
789        }
790    }
791}
792
793fn lookup_exact_detach_cell<'a, EF, D>(
794    member: &'a LiveMember<EF>,
795    cell: &DetachCell<D>,
796    binding: &BindingState,
797    request: &DetachRequest,
798    request_verifier: D,
799    observer_progress: DeliverySeq,
800) -> Option<DetachLookupResult<'a, EF, D>>
801where
802    D: Copy + Eq,
803{
804    // A detach cell deliberately does not duplicate the conversation id.  The
805    // resolved durable member therefore supplies that part of the authority;
806    // never reflect a caller-selected conversation into an exact-token reply.
807    if request.conversation_id != member.conversation_id() {
808        return None;
809    }
810
811    match cell {
812        DetachCell::Empty(_) => None,
813        DetachCell::Pending(pending) => {
814            Some(pending.verify_exact(request, request_verifier).map_or_else(
815                |_| stale_detach(request, member.generation()),
816                |verified| {
817                    DetachLookupResult::PendingReplayRequired(verified.prepare_replay(
818                        request.conversation_id,
819                        *binding,
820                        observer_progress,
821                    ))
822                },
823            ))
824        }
825        DetachCell::Committed(committed) => Some(
826            committed
827                .verify_exact(request, request_verifier)
828                .map_or_else(
829                    |_| stale_detach(request, member.generation()),
830                    |verified| {
831                        DetachLookupResult::DetachCommitted(
832                            verified.outcome(request.conversation_id),
833                        )
834                    },
835                ),
836        ),
837        DetachCell::Terminalized(terminalized) => Some(
838            terminalized
839                .verify_exact(request, request_verifier)
840                .map_or_else(
841                    |_| stale_detach(request, member.generation()),
842                    |verified| {
843                        let outcome = verified.outcome(
844                            request.conversation_id,
845                            member.generation(),
846                            binding_view(member, binding),
847                        );
848                        DetachLookupResult::StaleAuthority(
849                            DetachStaleAuthority::TerminalizedDetachCell(outcome),
850                        )
851                    },
852                ),
853        ),
854    }
855}
856
857/// Result of the permanent Leave-token verifier supplied by the consuming
858/// cryptographic layer.
859#[derive(Clone, Copy, Debug, PartialEq, Eq)]
860pub enum LeaveSecretProof {
861    /// The presented secret does not match the tombstone/live credential.
862    Mismatch,
863    /// The presented secret proof matches.
864    Verified,
865}
866
867/// Total result of Leave participant/token/binding lookup.
868#[derive(Clone, Debug, PartialEq, Eq)]
869pub enum LeaveLookupResult<'a, EF> {
870    /// Exact committed Leave token replayed its permanent result.
871    LeaveCommitted(LeaveCommitted),
872    /// Exact committed token had verified secret but changed generation.
873    AttemptTokenBodyConflict(AttemptTokenBodyConflict),
874    /// Live or committed-tombstone authority was stale.
875    StaleAuthority(LeaveStaleAuthority),
876    /// A different token resolved to a tombstone.
877    Retired(Retired),
878    /// No participant identity or tombstone exists.
879    ParticipantUnknown(ParticipantUnknown),
880    /// A live bound member was addressed from a different binding epoch.
881    NoBinding(NoBinding),
882    /// A bound Leave may execute its remaining checks.
883    AuthorizedBound {
884        /// Verified live membership.
885        member: &'a LiveMember<EF>,
886        /// Exact current binding authority.
887        binding: ActiveBinding,
888    },
889    /// A detached Leave may execute its remaining checks without acquiring a
890    /// binding or cursor authority.
891    AuthorizedDetached {
892        /// Verified detached live membership.
893        member: &'a LiveMember<EF>,
894    },
895}
896
897/// Applies the committed-Leave exception, tombstone precedence, and live Leave
898/// authority/binding order.
899///
900/// `request_binding_epoch` is the binding epoch carried by the receiving
901/// connection's serialized context. It is absent for the explicitly permitted
902/// detached Leave path.
903#[must_use]
904pub fn lookup_leave<'a, EF, V, LF>(
905    identity: PresentedIdentity<'a, EF, V, LF>,
906    binding: &BindingState,
907    request_binding_epoch: Option<crate::wire::BindingEpoch>,
908    request: &LeaveRequest,
909    secret_proof: LeaveSecretProof,
910) -> LeaveLookupResult<'a, EF> {
911    match identity {
912        PresentedIdentity::Retired(tombstone) => {
913            lookup_retired_leave(tombstone, request, secret_proof)
914        }
915        PresentedIdentity::Absent => LeaveLookupResult::ParticipantUnknown(unknown_leave(request)),
916        PresentedIdentity::Live(member) => {
917            if member.conversation_id() != request.conversation_id
918                || member.participant_id() != request.participant_id
919            {
920                return LeaveLookupResult::ParticipantUnknown(unknown_leave(request));
921            }
922
923            let generation_mismatch = request.capability_generation != member.generation();
924            let secret_mismatch = secret_proof == LeaveSecretProof::Mismatch;
925            if generation_mismatch || secret_mismatch {
926                return LeaveLookupResult::StaleAuthority(live_leave_stale(
927                    request,
928                    member.generation(),
929                ));
930            }
931
932            match binding {
933                BindingState::Bound(active)
934                    if active.conversation_id == request.conversation_id
935                        && active.participant_id == request.participant_id
936                        && request_binding_epoch == Some(active.binding_epoch) =>
937                {
938                    LeaveLookupResult::AuthorizedBound {
939                        member,
940                        binding: *active,
941                    }
942                }
943                BindingState::Bound(_) => LeaveLookupResult::NoBinding(no_binding_leave(request)),
944                BindingState::Detached | BindingState::PendingFinalization(_) => {
945                    LeaveLookupResult::AuthorizedDetached { member }
946                }
947            }
948        }
949    }
950}
951
952fn lookup_retired_leave<'a, EF, V, LF>(
953    tombstone: &RetiredIdentity<EF, V, LF>,
954    request: &LeaveRequest,
955    secret_proof: LeaveSecretProof,
956) -> LeaveLookupResult<'a, EF> {
957    if request.conversation_id != tombstone.conversation_id()
958        || request.participant_id != tombstone.participant_id()
959    {
960        return LeaveLookupResult::ParticipantUnknown(unknown_leave(request));
961    }
962
963    if request.leave_attempt_token != tombstone.leave_attempt_token() {
964        return LeaveLookupResult::Retired(retired_leave(tombstone, request));
965    }
966
967    if secret_proof == LeaveSecretProof::Mismatch {
968        return LeaveLookupResult::StaleAuthority(LeaveStaleAuthority::CommittedLeaveTombstone {
969            conversation_id: request.conversation_id,
970            participant_id: request.participant_id,
971            presented_generation: request.capability_generation,
972            leave_attempt_token: request.leave_attempt_token,
973            retired_generation: tombstone.retired_generation(),
974        });
975    }
976
977    if request.capability_generation != tombstone.committed_result().presented_generation() {
978        return LeaveLookupResult::AttemptTokenBodyConflict(AttemptTokenBodyConflict::Leave {
979            token: request.leave_attempt_token,
980            conversation_id: request.conversation_id,
981            presented_participant_id: request.participant_id,
982            presented_generation: request.capability_generation,
983        });
984    }
985
986    LeaveLookupResult::LeaveCommitted(tombstone.committed_result().clone())
987}
988
989fn receipt_binding_is_current(
990    binding: &BindingState,
991    conversation_id: crate::wire::ConversationId,
992    participant_id: crate::wire::ParticipantId,
993    origin_binding_epoch: BindingEpoch,
994) -> bool {
995    match binding {
996        BindingState::Bound(active) => {
997            active.conversation_id == conversation_id
998                && active.participant_id == participant_id
999                && active.binding_epoch == origin_binding_epoch
1000        }
1001        BindingState::Detached | BindingState::PendingFinalization(_) => false,
1002    }
1003}
1004
1005const fn enrollment_envelope(request: &EnrollmentRequest) -> EnrollmentEnvelope {
1006    EnrollmentEnvelope {
1007        conversation_id: request.conversation_id,
1008        enrollment_token: request.enrollment_token,
1009    }
1010}
1011
1012const fn attach_envelope(request: &CredentialAttachRequest) -> AttachEnvelope {
1013    AttachEnvelope {
1014        conversation_id: request.conversation_id,
1015        participant_id: request.participant_id,
1016        capability_generation: request.capability_generation,
1017        attach_attempt_token: request.attach_attempt_token,
1018        accept_marker_delivery_seq: request.accept_marker_delivery_seq,
1019    }
1020}
1021
1022const fn retired_enrollment<EF, V, LF>(
1023    tombstone: &RetiredIdentity<EF, V, LF>,
1024    request: &EnrollmentRequest,
1025) -> Retired {
1026    Retired::Enrollment {
1027        request: enrollment_envelope(request),
1028        participant_id: tombstone.participant_id(),
1029        retired_generation: tombstone.retired_generation(),
1030    }
1031}
1032
1033const fn retired_attach<EF, V, LF>(
1034    tombstone: &RetiredIdentity<EF, V, LF>,
1035    request: &CredentialAttachRequest,
1036) -> Retired {
1037    Retired::Participant {
1038        request: ParticipantReferenceEnvelope::CredentialAttach(attach_envelope(request)),
1039        retired_generation: tombstone.retired_generation(),
1040    }
1041}
1042
1043const fn unknown_attach(request: &CredentialAttachRequest) -> ParticipantUnknown {
1044    ParticipantUnknown {
1045        request: ParticipantReferenceEnvelope::CredentialAttach(attach_envelope(request)),
1046    }
1047}
1048
1049const fn stale_attach(
1050    request: &CredentialAttachRequest,
1051    current_generation: Generation,
1052) -> StaleAuthority {
1053    StaleAuthority::Live {
1054        request: CommonStaleAuthorityEnvelope::CredentialAttach(attach_envelope(request)),
1055        current_generation,
1056    }
1057}
1058
1059const fn binding_request_conversation(
1060    request: &ParticipantBindingRequest,
1061) -> crate::wire::ConversationId {
1062    match request {
1063        ParticipantBindingRequest::ParticipantAck(request) => request.conversation_id,
1064        ParticipantBindingRequest::MarkerAck(request) => request.conversation_id,
1065        ParticipantBindingRequest::RecordAdmission(request) => request.conversation_id,
1066    }
1067}
1068
1069const fn binding_request_participant(
1070    request: &ParticipantBindingRequest,
1071) -> crate::wire::ParticipantId {
1072    match request {
1073        ParticipantBindingRequest::ParticipantAck(request) => request.participant_id,
1074        ParticipantBindingRequest::MarkerAck(request) => request.participant_id,
1075        ParticipantBindingRequest::RecordAdmission(request) => request.participant_id,
1076    }
1077}
1078
1079const fn binding_request_generation(request: &ParticipantBindingRequest) -> Generation {
1080    match request {
1081        ParticipantBindingRequest::ParticipantAck(request) => request.capability_generation,
1082        ParticipantBindingRequest::MarkerAck(request) => request.capability_generation,
1083        ParticipantBindingRequest::RecordAdmission(request) => request.capability_generation,
1084    }
1085}
1086
1087const fn participant_ack_envelope(request: &ParticipantAck) -> ParticipantAckEnvelope {
1088    ParticipantAckEnvelope {
1089        conversation_id: request.conversation_id,
1090        participant_id: request.participant_id,
1091        capability_generation: request.capability_generation,
1092        through_seq: request.through_seq,
1093    }
1094}
1095
1096const fn marker_ack_envelope(request: &MarkerAck) -> MarkerAckEnvelope {
1097    MarkerAckEnvelope {
1098        conversation_id: request.conversation_id,
1099        participant_id: request.participant_id,
1100        capability_generation: request.capability_generation,
1101        marker_delivery_seq: request.marker_delivery_seq,
1102    }
1103}
1104
1105const fn record_admission_envelope(request: &RecordAdmission) -> RecordAdmissionEnvelope {
1106    RecordAdmissionEnvelope {
1107        conversation_id: request.conversation_id,
1108        participant_id: request.participant_id,
1109        capability_generation: request.capability_generation,
1110        record_admission_attempt_token: request.record_admission_attempt_token,
1111    }
1112}
1113
1114const fn participant_reference(
1115    request: &ParticipantBindingRequest,
1116) -> ParticipantReferenceEnvelope {
1117    match request {
1118        ParticipantBindingRequest::ParticipantAck(request) => {
1119            ParticipantReferenceEnvelope::ParticipantAck(participant_ack_envelope(request))
1120        }
1121        ParticipantBindingRequest::MarkerAck(request) => {
1122            ParticipantReferenceEnvelope::MarkerAck(marker_ack_envelope(request))
1123        }
1124        ParticipantBindingRequest::RecordAdmission(request) => {
1125            ParticipantReferenceEnvelope::RecordAdmission(record_admission_envelope(request))
1126        }
1127    }
1128}
1129
1130const fn binding_required_envelope(request: &ParticipantBindingRequest) -> BindingRequiredEnvelope {
1131    match request {
1132        ParticipantBindingRequest::ParticipantAck(request) => {
1133            BindingRequiredEnvelope::ParticipantAck(participant_ack_envelope(request))
1134        }
1135        ParticipantBindingRequest::MarkerAck(request) => {
1136            BindingRequiredEnvelope::MarkerAck(marker_ack_envelope(request))
1137        }
1138        ParticipantBindingRequest::RecordAdmission(request) => {
1139            BindingRequiredEnvelope::RecordAdmission(record_admission_envelope(request))
1140        }
1141    }
1142}
1143
1144const fn common_stale_envelope(
1145    request: &ParticipantBindingRequest,
1146) -> CommonStaleAuthorityEnvelope {
1147    match request {
1148        ParticipantBindingRequest::ParticipantAck(request) => {
1149            CommonStaleAuthorityEnvelope::ParticipantAck(participant_ack_envelope(request))
1150        }
1151        ParticipantBindingRequest::MarkerAck(request) => {
1152            CommonStaleAuthorityEnvelope::MarkerAck(marker_ack_envelope(request))
1153        }
1154        ParticipantBindingRequest::RecordAdmission(request) => {
1155            CommonStaleAuthorityEnvelope::RecordAdmission(record_admission_envelope(request))
1156        }
1157    }
1158}
1159
1160fn binding_view<EF>(member: &LiveMember<EF>, binding: &BindingState) -> BindingStateView {
1161    match binding {
1162        BindingState::Bound(active)
1163            if active.participant_id == member.participant_id()
1164                && active.conversation_id == member.conversation_id()
1165                && active.binding_epoch.capability_generation == member.generation() =>
1166        {
1167            BindingStateView::Bound {
1168                current_binding_epoch: active.binding_epoch,
1169            }
1170        }
1171        BindingState::Detached | BindingState::PendingFinalization(_) | BindingState::Bound(_) => {
1172            BindingStateView::Detached
1173        }
1174    }
1175}
1176
1177const fn detach_envelope(request: &DetachRequest) -> DetachEnvelope {
1178    DetachEnvelope {
1179        conversation_id: request.conversation_id,
1180        participant_id: request.participant_id,
1181        capability_generation: request.capability_generation,
1182        detach_attempt_token: request.detach_attempt_token,
1183    }
1184}
1185
1186const fn leave_envelope(request: &LeaveRequest) -> LeaveEnvelope {
1187    LeaveEnvelope {
1188        conversation_id: request.conversation_id,
1189        participant_id: request.participant_id,
1190        capability_generation: request.capability_generation,
1191        leave_attempt_token: request.leave_attempt_token,
1192    }
1193}
1194
1195const fn retired_detach<EF, V, LF>(
1196    tombstone: &RetiredIdentity<EF, V, LF>,
1197    request: &DetachRequest,
1198) -> Retired {
1199    Retired::Participant {
1200        request: ParticipantReferenceEnvelope::Detach(detach_envelope(request)),
1201        retired_generation: tombstone.retired_generation(),
1202    }
1203}
1204
1205const fn retired_leave<EF, V, LF>(
1206    tombstone: &RetiredIdentity<EF, V, LF>,
1207    request: &LeaveRequest,
1208) -> Retired {
1209    Retired::Participant {
1210        request: ParticipantReferenceEnvelope::Leave(leave_envelope(request)),
1211        retired_generation: tombstone.retired_generation(),
1212    }
1213}
1214
1215const fn unknown_detach(request: &DetachRequest) -> ParticipantUnknown {
1216    ParticipantUnknown {
1217        request: ParticipantReferenceEnvelope::Detach(detach_envelope(request)),
1218    }
1219}
1220
1221const fn unknown_leave(request: &LeaveRequest) -> ParticipantUnknown {
1222    ParticipantUnknown {
1223        request: ParticipantReferenceEnvelope::Leave(leave_envelope(request)),
1224    }
1225}
1226
1227const fn no_binding_detach(request: &DetachRequest) -> NoBinding {
1228    NoBinding {
1229        request: BindingRequiredEnvelope::Detach(detach_envelope(request)),
1230    }
1231}
1232
1233const fn no_binding_leave(request: &LeaveRequest) -> NoBinding {
1234    NoBinding {
1235        request: BindingRequiredEnvelope::Leave(leave_envelope(request)),
1236    }
1237}
1238
1239const fn stale_detach<'a, EF, D>(
1240    request: &DetachRequest,
1241    current_generation: Generation,
1242) -> DetachLookupResult<'a, EF, D> {
1243    DetachLookupResult::StaleAuthority(DetachStaleAuthority::Live {
1244        conversation_id: request.conversation_id,
1245        participant_id: request.participant_id,
1246        capability_generation: request.capability_generation,
1247        detach_attempt_token: request.detach_attempt_token,
1248        current_generation,
1249    })
1250}
1251
1252const fn live_leave_stale(
1253    request: &LeaveRequest,
1254    current_generation: Generation,
1255) -> LeaveStaleAuthority {
1256    LeaveStaleAuthority::Live {
1257        conversation_id: request.conversation_id,
1258        participant_id: request.participant_id,
1259        presented_generation: request.capability_generation,
1260        leave_attempt_token: request.leave_attempt_token,
1261        current_generation,
1262    }
1263}