Skip to main content

liminal_protocol/lifecycle/admission/
capacity.rs

1use core::num::NonZeroU64;
2
3use crate::wire::{
4    AttachEnvelope, CredentialAttachRequest, CredentialAttachResponse, EnrollmentEnvelope,
5    EnrollmentReceiptCapacityScope, EnrollmentRequest, EnrollmentResponse,
6    IdentityCapacityExceeded, IdentityCapacityScope, ParticipantId, ReceiptCapacityScope,
7};
8
9/// Invalid persisted occupancy for one signed nonzero capacity.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum CapacityCounterInvariantError {
12    /// Protocol capacity limits are nonzero.
13    ZeroLimit,
14    /// Persisted occupancy is greater than its signed limit.
15    OccupiedExceedsLimit {
16        /// Persisted occupancy.
17        occupied: u64,
18        /// Signed capacity limit.
19        limit: u64,
20    },
21}
22
23/// Validated occupancy bounded by one nonzero signed limit.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct CapacityCounter {
26    limit: NonZeroU64,
27    occupied: u64,
28}
29
30impl CapacityCounter {
31    /// Restores one counter only when its limit is nonzero and occupancy fits.
32    ///
33    /// # Errors
34    ///
35    /// Returns [`CapacityCounterInvariantError::ZeroLimit`] for a zero limit or
36    /// [`CapacityCounterInvariantError::OccupiedExceedsLimit`] when persisted
37    /// occupancy is outside the inclusive `0..=limit` domain.
38    pub const fn try_new(limit: u64, occupied: u64) -> Result<Self, CapacityCounterInvariantError> {
39        let Some(limit) = NonZeroU64::new(limit) else {
40            return Err(CapacityCounterInvariantError::ZeroLimit);
41        };
42        if occupied > limit.get() {
43            return Err(CapacityCounterInvariantError::OccupiedExceedsLimit {
44                occupied,
45                limit: limit.get(),
46            });
47        }
48        Ok(Self { limit, occupied })
49    }
50
51    /// Returns the signed nonzero limit.
52    #[must_use]
53    pub const fn limit(self) -> u64 {
54        self.limit.get()
55    }
56
57    /// Returns current validated occupancy.
58    #[must_use]
59    pub const fn occupied(self) -> u64 {
60        self.occupied
61    }
62
63    /// Returns whether another row would exceed the signed limit.
64    #[must_use]
65    pub const fn is_full(self) -> bool {
66        self.occupied == self.limit.get()
67    }
68
69    const fn incremented(self) -> Option<Self> {
70        if self.is_full() {
71            return None;
72        }
73        Some(Self {
74            limit: self.limit,
75            occupied: self.occupied + 1,
76        })
77    }
78}
79
80/// Invalid restored occupancy for a participant that has not yet been minted.
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub enum FreshParticipantCapacityCounterInvariantError {
83    /// The underlying nonzero bounded counter is invalid.
84    Capacity(CapacityCounterInvariantError),
85    /// A not-yet-minted participant cannot already own receipt state.
86    Nonempty {
87        /// Invalid restored per-participant occupancy.
88        occupied: u64,
89    },
90}
91
92/// Provably empty, nonzero per-participant capacity for fresh enrollment.
93///
94/// This type removes the unreachable enrollment refusal arms while still
95/// forcing the successful transaction to reserve both new participant rows.
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub struct FreshParticipantCapacityCounter {
98    counter: CapacityCounter,
99}
100
101impl FreshParticipantCapacityCounter {
102    /// Restores a fresh-participant counter only at occupancy zero.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`FreshParticipantCapacityCounterInvariantError::Capacity`] for
107    /// an invalid base counter or
108    /// [`FreshParticipantCapacityCounterInvariantError::Nonempty`] when a
109    /// not-yet-minted participant already has a row.
110    pub const fn try_new(
111        limit: u64,
112        occupied: u64,
113    ) -> Result<Self, FreshParticipantCapacityCounterInvariantError> {
114        let counter = match CapacityCounter::try_new(limit, occupied) {
115            Ok(counter) => counter,
116            Err(error) => {
117                return Err(FreshParticipantCapacityCounterInvariantError::Capacity(
118                    error,
119                ));
120            }
121        };
122        if occupied != 0 {
123            return Err(FreshParticipantCapacityCounterInvariantError::Nonempty { occupied });
124        }
125        Ok(Self { counter })
126    }
127
128    /// Returns the signed nonzero per-participant limit.
129    #[must_use]
130    pub const fn limit(self) -> u64 {
131        self.counter.limit()
132    }
133
134    /// Returns the type-proven zero occupancy.
135    #[must_use]
136    pub const fn occupied(self) -> u64 {
137        self.counter.occupied()
138    }
139
140    const fn reserved(self) -> CapacityCounter {
141        CapacityCounter {
142            limit: self.counter.limit,
143            occupied: 1,
144        }
145    }
146}
147
148/// Whether a semantic request's conversation already owns a connection slot.
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub enum ConnectionConversationTracking {
151    /// The conversation is already counted and consumes no additional slot.
152    AlreadyTracked,
153    /// The conversation needs its first connection-local slot.
154    Untracked,
155}
156
157/// Atomic successful result of semantic connection-capacity admission.
158#[derive(Clone, Copy, Debug, PartialEq, Eq)]
159pub struct ConnectionConversationCapacityCommit {
160    resulting: CapacityCounter,
161    newly_tracked: bool,
162}
163
164impl ConnectionConversationCapacityCommit {
165    /// Returns the complete post-operation connection occupancy.
166    #[must_use]
167    pub const fn resulting(self) -> CapacityCounter {
168        self.resulting
169    }
170
171    /// Returns whether the operation must install a new conversation slot.
172    #[must_use]
173    pub const fn newly_tracked(self) -> bool {
174        self.newly_tracked
175    }
176}
177
178/// Stage-6 semantic connection-capacity result.
179///
180/// The refusal arm carries only the request-independent capacity fact; the
181/// invoking operation mints its request-bound `0x0102` wire outcome from its
182/// own exact envelope plus this signed limit, so the triggering envelope is
183/// never duplicated through this shared selector.
184#[derive(Clone, Debug, PartialEq, Eq)]
185pub enum SemanticConnectionCapacityDecision {
186    /// Existing or newly reserved conversation capacity may commit.
187    Commit(ConnectionConversationCapacityCommit),
188    /// The untracked conversation would exceed the signed limit.
189    Respond {
190        /// Signed connection-conversation limit that is full.
191        limit: u64,
192    },
193}
194
195/// Applies semantic connection-conversation capacity before participant mutation.
196///
197/// An already tracked conversation succeeds without incrementing the counter,
198/// even when capacity is full. An untracked conversation either returns the
199/// complete incremented counter or the signed limit for the caller's exact
200/// request-bound `0x0102` wire outcome.
201#[must_use]
202pub const fn select_semantic_connection_capacity(
203    tracking: ConnectionConversationTracking,
204    current: CapacityCounter,
205) -> SemanticConnectionCapacityDecision {
206    match tracking {
207        ConnectionConversationTracking::AlreadyTracked => {
208            SemanticConnectionCapacityDecision::Commit(ConnectionConversationCapacityCommit {
209                resulting: current,
210                newly_tracked: false,
211            })
212        }
213        ConnectionConversationTracking::Untracked => {
214            let Some(resulting) = current.incremented() else {
215                return SemanticConnectionCapacityDecision::Respond {
216                    limit: current.limit(),
217                };
218            };
219            SemanticConnectionCapacityDecision::Commit(ConnectionConversationCapacityCommit {
220                resulting,
221                newly_tracked: true,
222            })
223        }
224    }
225}
226
227/// Current participant occupancy of one connection/conversation binding slot.
228#[derive(Clone, Copy, Debug, PartialEq, Eq)]
229pub enum BindingSlotOccupancy {
230    /// No participant currently occupies the slot.
231    Empty,
232    /// One participant currently occupies the slot.
233    Occupied {
234        /// Occupying participant, used only for same-participant rotation.
235        participant_id: ParticipantId,
236    },
237}
238
239/// Stage-6 participant binding-slot result, bound to the requesting
240/// operation's response authority.
241#[derive(Clone, Debug, PartialEq, Eq)]
242pub enum BindingSlotDecision<R> {
243    /// The binding operation may continue.
244    Available,
245    /// Exact request-bound binding-slot refusal.
246    Respond(R),
247}
248
249/// Selects enrollment binding-slot occupancy without revealing its occupant.
250#[must_use]
251pub const fn select_enrollment_binding_slot(
252    request: &EnrollmentRequest,
253    occupancy: BindingSlotOccupancy,
254) -> BindingSlotDecision<EnrollmentResponse> {
255    match occupancy {
256        BindingSlotOccupancy::Empty => BindingSlotDecision::Available,
257        BindingSlotOccupancy::Occupied { .. } => BindingSlotDecision::Respond(
258            EnrollmentResponse::connection_conversation_binding_occupied(&enrollment_envelope(
259                request,
260            )),
261        ),
262    }
263}
264
265/// Selects credential-attach binding occupancy, permitting only an empty slot
266/// or rotation of the same presented participant.
267#[must_use]
268pub const fn select_credential_attach_binding_slot(
269    request: &CredentialAttachRequest,
270    occupancy: BindingSlotOccupancy,
271) -> BindingSlotDecision<CredentialAttachResponse> {
272    match occupancy {
273        BindingSlotOccupancy::Empty => BindingSlotDecision::Available,
274        BindingSlotOccupancy::Occupied { participant_id }
275            if participant_id == request.participant_id =>
276        {
277            BindingSlotDecision::Available
278        }
279        BindingSlotOccupancy::Occupied { .. } => BindingSlotDecision::Respond(
280            CredentialAttachResponse::connection_conversation_binding_occupied(&attach_envelope(
281                request,
282            )),
283        ),
284    }
285}
286
287/// All seven stage-8 counters for a fresh enrollment.
288///
289/// Only five can refuse. The two per-participant counters use
290/// [`FreshParticipantCapacityCounter`], proving their occupancy is zero and
291/// their limits nonzero before identity mint.
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
293pub struct EnrollmentCapacityCounters {
294    identity_server: CapacityCounter,
295    identity_conversation: CapacityCounter,
296    live_receipt_server: CapacityCounter,
297    live_receipt_participant: FreshParticipantCapacityCounter,
298    provenance_server: CapacityCounter,
299    provenance_conversation: CapacityCounter,
300    provenance_participant: FreshParticipantCapacityCounter,
301}
302
303impl EnrollmentCapacityCounters {
304    /// Creates the complete reachable enrollment counter snapshot.
305    #[must_use]
306    pub const fn new(
307        identity_server: CapacityCounter,
308        identity_conversation: CapacityCounter,
309        live_receipt_server: CapacityCounter,
310        live_receipt_participant: FreshParticipantCapacityCounter,
311        provenance_server: CapacityCounter,
312        provenance_conversation: CapacityCounter,
313        provenance_participant: FreshParticipantCapacityCounter,
314    ) -> Self {
315        Self {
316            identity_server,
317            identity_conversation,
318            live_receipt_server,
319            live_receipt_participant,
320            provenance_server,
321            provenance_conversation,
322            provenance_participant,
323        }
324    }
325
326    /// Returns server-wide identity occupancy.
327    #[must_use]
328    pub const fn identity_server(self) -> CapacityCounter {
329        self.identity_server
330    }
331
332    /// Returns conversation identity occupancy.
333    #[must_use]
334    pub const fn identity_conversation(self) -> CapacityCounter {
335        self.identity_conversation
336    }
337
338    /// Returns server-wide live-receipt occupancy.
339    #[must_use]
340    pub const fn live_receipt_server(self) -> CapacityCounter {
341        self.live_receipt_server
342    }
343
344    /// Returns the provably empty participant live-receipt capacity.
345    #[must_use]
346    pub const fn live_receipt_participant(self) -> FreshParticipantCapacityCounter {
347        self.live_receipt_participant
348    }
349
350    /// Returns server-wide provenance occupancy.
351    #[must_use]
352    pub const fn provenance_server(self) -> CapacityCounter {
353        self.provenance_server
354    }
355
356    /// Returns conversation provenance occupancy.
357    #[must_use]
358    pub const fn provenance_conversation(self) -> CapacityCounter {
359        self.provenance_conversation
360    }
361
362    /// Returns the provably empty participant provenance capacity.
363    #[must_use]
364    pub const fn provenance_participant(self) -> FreshParticipantCapacityCounter {
365        self.provenance_participant
366    }
367}
368
369/// All seven post-enrollment identity and receipt/provenance counters.
370#[derive(Clone, Copy, Debug, PartialEq, Eq)]
371pub struct ResultingEnrollmentCapacityCounters {
372    identity_server: CapacityCounter,
373    identity_conversation: CapacityCounter,
374    live_receipt_server: CapacityCounter,
375    live_receipt_participant: CapacityCounter,
376    provenance_server: CapacityCounter,
377    provenance_conversation: CapacityCounter,
378    provenance_participant: CapacityCounter,
379}
380
381impl ResultingEnrollmentCapacityCounters {
382    /// Returns server-wide identity occupancy.
383    #[must_use]
384    pub const fn identity_server(self) -> CapacityCounter {
385        self.identity_server
386    }
387
388    /// Returns conversation identity occupancy.
389    #[must_use]
390    pub const fn identity_conversation(self) -> CapacityCounter {
391        self.identity_conversation
392    }
393
394    /// Returns server-wide live-receipt occupancy.
395    #[must_use]
396    pub const fn live_receipt_server(self) -> CapacityCounter {
397        self.live_receipt_server
398    }
399
400    /// Returns the newly minted participant's live-receipt occupancy.
401    #[must_use]
402    pub const fn live_receipt_participant(self) -> CapacityCounter {
403        self.live_receipt_participant
404    }
405
406    /// Returns server-wide provenance occupancy.
407    #[must_use]
408    pub const fn provenance_server(self) -> CapacityCounter {
409        self.provenance_server
410    }
411
412    /// Returns conversation provenance occupancy.
413    #[must_use]
414    pub const fn provenance_conversation(self) -> CapacityCounter {
415        self.provenance_conversation
416    }
417
418    /// Returns the newly minted participant's provenance occupancy.
419    #[must_use]
420    pub const fn provenance_participant(self) -> CapacityCounter {
421        self.provenance_participant
422    }
423}
424
425/// Atomic successful enrollment capacity reservation.
426#[derive(Clone, Copy, Debug, PartialEq, Eq)]
427pub struct EnrollmentCapacityCommit {
428    resulting: ResultingEnrollmentCapacityCounters,
429}
430
431impl EnrollmentCapacityCommit {
432    /// Returns every incremented enrollment counter as one commit value.
433    #[must_use]
434    pub const fn resulting(self) -> ResultingEnrollmentCapacityCounters {
435        self.resulting
436    }
437}
438
439/// Exhaustive stage-8 enrollment runtime-capacity result.
440#[derive(Clone, Debug, PartialEq, Eq)]
441pub enum EnrollmentCapacityDecision {
442    /// All seven reservations may commit together.
443    Commit(EnrollmentCapacityCommit),
444    /// Exact first-full identity or receipt scope, bound to enrollment.
445    Respond(EnrollmentResponse),
446}
447
448/// Applies the fixed enrollment runtime-capacity order atomically.
449///
450/// The order is identity Server, identity Conversation, `LiveReceiptServer`,
451/// `ProvenanceServer`, then `ProvenanceConversation`. A refusal exposes only
452/// the first full scope; success carries every post-increment counter together.
453#[must_use]
454pub const fn select_enrollment_capacity(
455    request: &EnrollmentRequest,
456    current: EnrollmentCapacityCounters,
457) -> EnrollmentCapacityDecision {
458    let Some(identity_server) = current.identity_server.incremented() else {
459        return enrollment_identity_refusal(
460            request,
461            IdentityCapacityScope::Server,
462            current.identity_server,
463        );
464    };
465    let Some(identity_conversation) = current.identity_conversation.incremented() else {
466        return enrollment_identity_refusal(
467            request,
468            IdentityCapacityScope::Conversation,
469            current.identity_conversation,
470        );
471    };
472    let Some(live_receipt_server) = current.live_receipt_server.incremented() else {
473        return enrollment_receipt_refusal(
474            request,
475            EnrollmentReceiptCapacityScope::LiveReceiptServer,
476            current.live_receipt_server,
477        );
478    };
479    let Some(provenance_server) = current.provenance_server.incremented() else {
480        return enrollment_receipt_refusal(
481            request,
482            EnrollmentReceiptCapacityScope::ProvenanceServer,
483            current.provenance_server,
484        );
485    };
486    let Some(provenance_conversation) = current.provenance_conversation.incremented() else {
487        return enrollment_receipt_refusal(
488            request,
489            EnrollmentReceiptCapacityScope::ProvenanceConversation,
490            current.provenance_conversation,
491        );
492    };
493
494    EnrollmentCapacityDecision::Commit(EnrollmentCapacityCommit {
495        resulting: ResultingEnrollmentCapacityCounters {
496            identity_server,
497            identity_conversation,
498            live_receipt_server,
499            live_receipt_participant: current.live_receipt_participant.reserved(),
500            provenance_server,
501            provenance_conversation,
502            provenance_participant: current.provenance_participant.reserved(),
503        },
504    })
505}
506
507/// The five ordered receipt/provenance counters for credential attach.
508#[derive(Clone, Copy, Debug, PartialEq, Eq)]
509pub struct CredentialAttachCapacityCounters {
510    live_receipt_server: CapacityCounter,
511    live_receipt_participant: CapacityCounter,
512    provenance_server: CapacityCounter,
513    provenance_conversation: CapacityCounter,
514    provenance_participant: CapacityCounter,
515}
516
517impl CredentialAttachCapacityCounters {
518    /// Creates the complete credential-attach counter snapshot.
519    #[must_use]
520    pub const fn new(
521        live_receipt_server: CapacityCounter,
522        live_receipt_participant: CapacityCounter,
523        provenance_server: CapacityCounter,
524        provenance_conversation: CapacityCounter,
525        provenance_participant: CapacityCounter,
526    ) -> Self {
527        Self {
528            live_receipt_server,
529            live_receipt_participant,
530            provenance_server,
531            provenance_conversation,
532            provenance_participant,
533        }
534    }
535
536    /// Returns server-wide live-receipt occupancy.
537    #[must_use]
538    pub const fn live_receipt_server(self) -> CapacityCounter {
539        self.live_receipt_server
540    }
541
542    /// Returns participant live-receipt occupancy.
543    #[must_use]
544    pub const fn live_receipt_participant(self) -> CapacityCounter {
545        self.live_receipt_participant
546    }
547
548    /// Returns server-wide provenance occupancy.
549    #[must_use]
550    pub const fn provenance_server(self) -> CapacityCounter {
551        self.provenance_server
552    }
553
554    /// Returns conversation provenance occupancy.
555    #[must_use]
556    pub const fn provenance_conversation(self) -> CapacityCounter {
557        self.provenance_conversation
558    }
559
560    /// Returns participant provenance occupancy.
561    #[must_use]
562    pub const fn provenance_participant(self) -> CapacityCounter {
563        self.provenance_participant
564    }
565}
566
567/// Atomic successful credential-attach capacity reservation.
568#[derive(Clone, Copy, Debug, PartialEq, Eq)]
569pub struct CredentialAttachCapacityCommit {
570    resulting: CredentialAttachCapacityCounters,
571}
572
573impl CredentialAttachCapacityCommit {
574    /// Returns all five incremented receipt/provenance counters together.
575    #[must_use]
576    pub const fn resulting(self) -> CredentialAttachCapacityCounters {
577        self.resulting
578    }
579}
580
581/// Exhaustive stage-8 credential-attach runtime-capacity result.
582#[derive(Clone, Debug, PartialEq, Eq)]
583pub enum CredentialAttachCapacityDecision {
584    /// All five receipt/provenance reservations may commit together.
585    Commit(CredentialAttachCapacityCommit),
586    /// Exact first-full receipt/provenance scope, bound to credential attach.
587    Respond(CredentialAttachResponse),
588}
589
590/// Applies credential attach's exact five-scope runtime-capacity order.
591#[must_use]
592pub const fn select_credential_attach_capacity(
593    request: &CredentialAttachRequest,
594    current: CredentialAttachCapacityCounters,
595) -> CredentialAttachCapacityDecision {
596    let Some(live_receipt_server) = current.live_receipt_server.incremented() else {
597        return credential_attach_receipt_refusal(
598            request,
599            ReceiptCapacityScope::LiveReceiptServer,
600            current.live_receipt_server,
601        );
602    };
603    let Some(live_receipt_participant) = current.live_receipt_participant.incremented() else {
604        return credential_attach_receipt_refusal(
605            request,
606            ReceiptCapacityScope::LiveReceiptParticipant,
607            current.live_receipt_participant,
608        );
609    };
610    let Some(provenance_server) = current.provenance_server.incremented() else {
611        return credential_attach_receipt_refusal(
612            request,
613            ReceiptCapacityScope::ProvenanceServer,
614            current.provenance_server,
615        );
616    };
617    let Some(provenance_conversation) = current.provenance_conversation.incremented() else {
618        return credential_attach_receipt_refusal(
619            request,
620            ReceiptCapacityScope::ProvenanceConversation,
621            current.provenance_conversation,
622        );
623    };
624    let Some(provenance_participant) = current.provenance_participant.incremented() else {
625        return credential_attach_receipt_refusal(
626            request,
627            ReceiptCapacityScope::ProvenanceParticipant,
628            current.provenance_participant,
629        );
630    };
631
632    CredentialAttachCapacityDecision::Commit(CredentialAttachCapacityCommit {
633        resulting: CredentialAttachCapacityCounters {
634            live_receipt_server,
635            live_receipt_participant,
636            provenance_server,
637            provenance_conversation,
638            provenance_participant,
639        },
640    })
641}
642
643const fn enrollment_identity_refusal(
644    request: &EnrollmentRequest,
645    scope: IdentityCapacityScope,
646    counter: CapacityCounter,
647) -> EnrollmentCapacityDecision {
648    EnrollmentCapacityDecision::Respond(EnrollmentResponse::identity_capacity_exceeded(
649        IdentityCapacityExceeded {
650            request: enrollment_envelope(request),
651            scope,
652            limit: counter.limit(),
653            occupied: counter.occupied(),
654        },
655    ))
656}
657
658const fn enrollment_receipt_refusal(
659    request: &EnrollmentRequest,
660    scope: EnrollmentReceiptCapacityScope,
661    counter: CapacityCounter,
662) -> EnrollmentCapacityDecision {
663    EnrollmentCapacityDecision::Respond(EnrollmentResponse::receipt_capacity_exceeded(
664        enrollment_envelope(request),
665        scope,
666        counter.limit(),
667        counter.occupied(),
668    ))
669}
670
671const fn credential_attach_receipt_refusal(
672    request: &CredentialAttachRequest,
673    scope: ReceiptCapacityScope,
674    counter: CapacityCounter,
675) -> CredentialAttachCapacityDecision {
676    CredentialAttachCapacityDecision::Respond(CredentialAttachResponse::receipt_capacity_exceeded(
677        attach_envelope(request),
678        scope,
679        counter.limit(),
680        counter.occupied(),
681    ))
682}
683
684const fn enrollment_envelope(request: &EnrollmentRequest) -> EnrollmentEnvelope {
685    EnrollmentEnvelope {
686        conversation_id: request.conversation_id,
687        enrollment_token: request.enrollment_token,
688    }
689}
690
691const fn attach_envelope(request: &CredentialAttachRequest) -> AttachEnvelope {
692    AttachEnvelope {
693        conversation_id: request.conversation_id,
694        participant_id: request.participant_id,
695        capability_generation: request.capability_generation,
696        attach_attempt_token: request.attach_attempt_token,
697        accept_marker_delivery_seq: request.accept_marker_delivery_seq,
698    }
699}