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