Skip to main content

liminal_protocol/lifecycle/admission/
sequence.rs

1use alloc::boxed::Box;
2
3use crate::wire::{ConversationSequenceExhausted, SequenceAllocatingEnvelope, SequenceBudget};
4
5/// Edge-owned recovery sequence claims.
6///
7/// The frozen contract permits only the empty state or the DCR quartet's
8/// coupled `RS=1, RT=1` state. Fenced recovery consumes `RS` and atomically
9/// transfers `RT` into a normal terminal claim, so no durable half-pair exists.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum RecoverySequenceReserve {
12    /// No recovery attach or replacement-terminal sequence claim.
13    #[default]
14    None,
15    /// One recovery attach claim and one replacement-terminal claim.
16    DetachedCredentialRecovery,
17}
18
19impl RecoverySequenceReserve {
20    const fn claims(self) -> (u64, u64) {
21        match self {
22            Self::None => (0, 0),
23            Self::DetachedCredentialRecovery => (1, 1),
24        }
25    }
26}
27
28/// Primitive participant-lifecycle claims from which the sequence reserve is derived.
29///
30/// `E`, `L_other`, and all three products are deliberately absent from this
31/// input. They are derived by the protocol so a caller cannot provide a
32/// self-inconsistent canonical budget.
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
34pub struct SequenceClaims {
35    live_members: u64,
36    binding_terminals: u64,
37    markers: u64,
38    recovery: RecoverySequenceReserve,
39}
40
41impl SequenceClaims {
42    /// Creates the primitive `L`, `T`, `M`, and coupled recovery claim state.
43    #[must_use]
44    pub const fn new(
45        live_members: u64,
46        binding_terminals: u64,
47        markers: u64,
48        recovery: RecoverySequenceReserve,
49    ) -> Self {
50        Self {
51            live_members,
52            binding_terminals,
53            markers,
54            recovery,
55        }
56    }
57
58    /// Returns live members `L`; flat exit claims `E` are exactly this value.
59    #[must_use]
60    pub const fn live_members(self) -> u64 {
61        self.live_members
62    }
63
64    /// Returns binding-terminal claims `T`.
65    #[must_use]
66    pub const fn binding_terminals(self) -> u64 {
67        self.binding_terminals
68    }
69
70    /// Returns required-but-unwritten marker claims `M`.
71    #[must_use]
72    pub const fn markers(self) -> u64 {
73        self.markers
74    }
75
76    /// Returns the coupled recovery sequence reserve.
77    #[must_use]
78    pub const fn recovery(self) -> RecoverySequenceReserve {
79        self.recovery
80    }
81
82    /// Derives the canonical ten-field budget at `high_watermark`.
83    #[must_use]
84    pub fn budget(self, high_watermark: u64) -> SequenceBudget {
85        let live_members = u128::from(self.live_members);
86        let exits = self.live_members;
87        let exits_wide = u128::from(exits);
88        let terminals = u128::from(self.binding_terminals);
89        let (rs, rt) = self.recovery.claims();
90        let replacement_terminals = u128::from(rt);
91        let other_live_members = if live_members == 0 {
92            0
93        } else {
94            live_members - 1
95        };
96
97        SequenceBudget {
98            high_watermark,
99            remaining: u64::MAX - high_watermark,
100            e: exits,
101            t: self.binding_terminals,
102            m: self.markers,
103            rs,
104            rt,
105            l_times_t: live_members * terminals,
106            l_times_rt: live_members * replacement_terminals,
107            l_other_times_e: other_live_members * exits_wide,
108        }
109    }
110
111    /// Computes the exact reserve in the frozen canonical term order.
112    ///
113    /// The order is `E`, `T`, `M`, `RS`, `RT`, `L*T`, `L*RT`, then
114    /// `L_other*E`, with every scalar widened before addition. `None` means the
115    /// sum itself exceeds `u128`; each canonical product remains representable.
116    #[must_use]
117    pub fn checked_required_reserve(self) -> Option<u128> {
118        checked_required_reserve(&self.budget(0))
119    }
120}
121
122/// Invalid persisted sequence ledger.
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub enum SequenceLedgerInvariantError {
125    /// Claims exceed the exact sequence suffix that remains after the high watermark.
126    ClaimsExceedRemaining {
127        /// Canonical ten-field budget derived from the invalid state.
128        budget: Box<SequenceBudget>,
129        /// Exact required reserve, or `None` if its canonical u128 sum overflowed.
130        required_reserve: Option<u128>,
131    },
132}
133
134/// Validated delivery-sequence watermark and all unmaterialized claims.
135///
136/// Storage bindings may restore and inspect this factual snapshot, but cannot
137/// invoke the lower-level planner directly. Executable admission is owned by
138/// the protocol's total lifecycle operations.
139///
140/// ```compile_fail
141/// use liminal_protocol::lifecycle::SequenceLedger;
142///
143/// fn bypass_total_operation(ledger: SequenceLedger) {
144///     let _ = ledger.plan_ordinary_record(0);
145/// }
146/// ```
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub struct SequenceLedger {
149    high_watermark: u64,
150    claims: SequenceClaims,
151    required_reserve: u128,
152}
153
154impl SequenceLedger {
155    /// Restores a ledger only when its derived reserve fits the remaining suffix.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`SequenceLedgerInvariantError`] if the canonical reserve sum
160    /// overflows u128 or exceeds `u64::MAX - high_watermark`.
161    pub fn try_new(
162        high_watermark: u64,
163        claims: SequenceClaims,
164    ) -> Result<Self, SequenceLedgerInvariantError> {
165        let budget = claims.budget(high_watermark);
166        let required_reserve = checked_required_reserve(&budget);
167        let Some(required_reserve) = required_reserve else {
168            return Err(SequenceLedgerInvariantError::ClaimsExceedRemaining {
169                budget: Box::new(budget),
170                required_reserve,
171            });
172        };
173        if required_reserve > u128::from(budget.remaining) {
174            return Err(SequenceLedgerInvariantError::ClaimsExceedRemaining {
175                budget: Box::new(budget),
176                required_reserve: Some(required_reserve),
177            });
178        }
179        Ok(Self {
180            high_watermark,
181            claims,
182            required_reserve,
183        })
184    }
185
186    /// Returns the greatest allocated delivery sequence.
187    #[must_use]
188    pub const fn high_watermark(self) -> u64 {
189        self.high_watermark
190    }
191
192    /// Returns the primitive claims from which the reserve is derived.
193    #[must_use]
194    pub const fn claims(self) -> SequenceClaims {
195        self.claims
196    }
197
198    /// Returns the canonical ten-field budget.
199    #[must_use]
200    pub fn budget(self) -> SequenceBudget {
201        self.claims.budget(self.high_watermark)
202    }
203
204    /// Returns the exact checked-wide reserve owned by the claims.
205    #[must_use]
206    pub const fn required_reserve(self) -> u128 {
207        self.required_reserve
208    }
209
210    /// Plans optional enrollment: one record, one member, one terminal claim,
211    /// and the protocol-computed new marker claims.
212    ///
213    /// Existing recovery claims are preserved. `E`, `L_other`, and all product
214    /// terms remain derived from the resulting primitive state.
215    ///
216    /// # Errors
217    ///
218    /// Returns the first arithmetic variant of [`SequenceAdmissionError`] whose
219    /// checked addition cannot be represented.
220    #[cfg(test)]
221    pub(crate) fn plan_enrollment(
222        self,
223        new_markers: u64,
224    ) -> Result<ResultingSequenceState, SequenceAdmissionError> {
225        self.plan_enrollment_with_recovery_quartet(new_markers, false)
226    }
227
228    /// Plans enrollment while allowing the protocol-owned closure projection to
229    /// endow the episode's sole coupled `RS=1,RT=1` reserve.
230    ///
231    /// The endowment selector is crate-private so a storage binding cannot mint
232    /// recovery claims from raw values.
233    pub(crate) fn plan_enrollment_with_recovery_quartet(
234        self,
235        new_markers: u64,
236        endow_recovery_quartet: bool,
237    ) -> Result<ResultingSequenceState, SequenceAdmissionError> {
238        self.ensure_quartet_can_be_endowed(endow_recovery_quartet)?;
239        let high_watermark = self.checked_high_watermark(1)?;
240        let live_members = self.claims.live_members.checked_add(1).ok_or(
241            SequenceAdmissionError::LiveMemberClaimOverflow {
242                live_members: self.claims.live_members,
243            },
244        )?;
245        let binding_terminals = self.claims.binding_terminals.checked_add(1).ok_or(
246            SequenceAdmissionError::BindingTerminalClaimOverflow {
247                binding_terminals: self.claims.binding_terminals,
248            },
249        )?;
250        let markers = self.checked_markers(new_markers)?;
251        Ok(ResultingSequenceState {
252            high_watermark,
253            claims: SequenceClaims {
254                live_members,
255                binding_terminals,
256                markers,
257                recovery: if endow_recovery_quartet {
258                    RecoverySequenceReserve::DetachedCredentialRecovery
259                } else {
260                    self.claims.recovery
261                },
262            },
263        })
264    }
265
266    /// Plans optional detached attach: one record, one terminal claim, and new markers.
267    ///
268    /// Membership and existing recovery claims are preserved.
269    ///
270    /// # Errors
271    ///
272    /// Returns the first arithmetic variant of [`SequenceAdmissionError`] whose
273    /// checked addition cannot be represented.
274    #[cfg(test)]
275    pub(crate) fn plan_detached_attach(
276        self,
277        new_markers: u64,
278    ) -> Result<ResultingSequenceState, SequenceAdmissionError> {
279        let high_watermark = self.checked_high_watermark(1)?;
280        let binding_terminals = self.claims.binding_terminals.checked_add(1).ok_or(
281            SequenceAdmissionError::BindingTerminalClaimOverflow {
282                binding_terminals: self.claims.binding_terminals,
283            },
284        )?;
285        let markers = self.checked_markers(new_markers)?;
286        Ok(ResultingSequenceState {
287            high_watermark,
288            claims: SequenceClaims {
289                binding_terminals,
290                markers,
291                ..self.claims
292            },
293        })
294    }
295
296    /// Plans optional supersession: two records and the new marker claims.
297    ///
298    /// The old terminal claim is consumed while the replacement binding creates
299    /// one, so `T` is unchanged. Membership and recovery claims are preserved.
300    ///
301    /// # Errors
302    ///
303    /// Returns the first arithmetic variant of [`SequenceAdmissionError`] whose
304    /// checked addition cannot be represented.
305    #[cfg(test)]
306    pub(crate) fn plan_supersession(
307        self,
308        new_markers: u64,
309    ) -> Result<ResultingSequenceState, SequenceAdmissionError> {
310        let high_watermark = self.checked_high_watermark(2)?;
311        let markers = self.checked_markers(new_markers)?;
312        Ok(ResultingSequenceState {
313            high_watermark,
314            claims: SequenceClaims {
315                markers,
316                ..self.claims
317            },
318        })
319    }
320
321    /// Plans ordinary admission: one record and the new marker claims.
322    ///
323    /// Membership, terminal, and recovery claims are unchanged.
324    ///
325    /// # Errors
326    ///
327    /// Returns the first arithmetic variant of [`SequenceAdmissionError`] whose
328    /// checked addition cannot be represented.
329    pub(crate) fn plan_ordinary_record(
330        self,
331        new_markers: u64,
332    ) -> Result<ResultingSequenceState, SequenceAdmissionError> {
333        let high_watermark = self.checked_high_watermark(1)?;
334        let markers = self.checked_markers(new_markers)?;
335        Ok(ResultingSequenceState {
336            high_watermark,
337            claims: SequenceClaims {
338                markers,
339                ..self.claims
340            },
341        })
342    }
343
344    const fn ensure_quartet_can_be_endowed(
345        self,
346        endow_recovery_quartet: bool,
347    ) -> Result<(), SequenceAdmissionError> {
348        if endow_recovery_quartet
349            && matches!(
350                self.claims.recovery,
351                RecoverySequenceReserve::DetachedCredentialRecovery
352            )
353        {
354            return Err(SequenceAdmissionError::RecoverySequenceReserveAlreadyPresent);
355        }
356        Ok(())
357    }
358
359    /// Applies fenced recovery from its coupled `RS=1, RT=1` reserve.
360    ///
361    /// The recovery Attached record consumes `RS`, while `RT` transfers into a
362    /// normal `T` claim for the recovered binding. The high watermark advances
363    /// once, recovery claims become empty, and no optional sequence allocation
364    /// is performed.
365    ///
366    /// # Errors
367    ///
368    /// Returns [`SequenceAdmissionError::RecoverySequenceReserveMissing`] if no
369    /// DCR pair exists, an arithmetic error if a checked addition fails, or
370    /// [`SequenceAdmissionError::RecoverySequenceInvariantViolation`] if the
371    /// exact reserved transfer did not preserve the ledger invariant.
372    pub(in crate::lifecycle) fn apply_fenced_recovery(
373        self,
374    ) -> Result<Self, SequenceAdmissionError> {
375        if self.claims.recovery != RecoverySequenceReserve::DetachedCredentialRecovery {
376            return Err(SequenceAdmissionError::RecoverySequenceReserveMissing);
377        }
378        let high_watermark = self.checked_high_watermark(1)?;
379        let binding_terminals = self.claims.binding_terminals.checked_add(1).ok_or(
380            SequenceAdmissionError::BindingTerminalClaimOverflow {
381                binding_terminals: self.claims.binding_terminals,
382            },
383        )?;
384        let claims = SequenceClaims {
385            binding_terminals,
386            recovery: RecoverySequenceReserve::None,
387            ..self.claims
388        };
389        let budget = claims.budget(high_watermark);
390        let required_reserve = checked_required_reserve(&budget)
391            .ok_or(SequenceAdmissionError::RecoverySequenceInvariantViolation)?;
392        if required_reserve > u128::from(budget.remaining) {
393            return Err(SequenceAdmissionError::RecoverySequenceInvariantViolation);
394        }
395        Ok(Self {
396            high_watermark,
397            claims,
398            required_reserve,
399        })
400    }
401
402    /// Applies fenced recovery while atomically materializing one separately
403    /// pending terminal. The old `T` claim is consumed and `RT` transfers into
404    /// the recovered binding's new `T`, so the terminal count is conserved.
405    /// Both terminal and Attached records advance the high watermark.
406    pub(in crate::lifecycle) fn apply_fenced_recovery_finalizing_pending(
407        self,
408    ) -> Result<Self, SequenceAdmissionError> {
409        if self.claims.recovery != RecoverySequenceReserve::DetachedCredentialRecovery {
410            return Err(SequenceAdmissionError::RecoverySequenceReserveMissing);
411        }
412        if self.claims.binding_terminals == 0 {
413            return Err(SequenceAdmissionError::RecoverySequenceInvariantViolation);
414        }
415        let high_watermark = self.checked_high_watermark(2)?;
416        let claims = SequenceClaims {
417            recovery: RecoverySequenceReserve::None,
418            ..self.claims
419        };
420        let budget = claims.budget(high_watermark);
421        let required_reserve = checked_required_reserve(&budget)
422            .ok_or(SequenceAdmissionError::RecoverySequenceInvariantViolation)?;
423        if required_reserve > u128::from(budget.remaining) {
424            return Err(SequenceAdmissionError::RecoverySequenceInvariantViolation);
425        }
426        Ok(Self {
427            high_watermark,
428            claims,
429            required_reserve,
430        })
431    }
432
433    fn checked_high_watermark(self, required_values: u64) -> Result<u64, SequenceAdmissionError> {
434        self.high_watermark.checked_add(required_values).ok_or(
435            SequenceAdmissionError::HighWatermarkOverflow {
436                high_watermark: self.high_watermark,
437                required_values,
438            },
439        )
440    }
441
442    fn checked_markers(self, new_markers: u64) -> Result<u64, SequenceAdmissionError> {
443        self.claims.markers.checked_add(new_markers).ok_or(
444            SequenceAdmissionError::MarkerClaimOverflow {
445                markers: self.claims.markers,
446                new_markers,
447            },
448        )
449    }
450}
451
452/// Protocol-produced proposed sequence state.
453///
454/// Construction is crate-private so a consuming server cannot invent the
455/// post-operation `L`, `T`, `M`, `RS`, or `RT` state used by admission.
456#[derive(Clone, Copy, Debug, PartialEq, Eq)]
457pub struct ResultingSequenceState {
458    high_watermark: u64,
459    claims: SequenceClaims,
460}
461
462/// Successful sequence-reserve admission.
463#[derive(Clone, Copy, Debug, PartialEq, Eq)]
464pub struct SequenceAdmission {
465    resulting: SequenceLedger,
466}
467
468impl SequenceAdmission {
469    /// Returns the complete validated proposed ledger.
470    #[must_use]
471    pub const fn resulting(self) -> SequenceLedger {
472        self.resulting
473    }
474}
475
476/// Sequence admission refusal.
477#[derive(Clone, Debug, PartialEq, Eq)]
478pub enum SequenceAdmissionError {
479    /// The proposed canonical reserve exceeds the resulting sequence suffix.
480    Exhausted(Box<ConversationSequenceExhausted>),
481    /// Appending the operation's record count would exceed the sequence domain.
482    HighWatermarkOverflow {
483        /// Current high watermark.
484        high_watermark: u64,
485        /// Number of consecutive values the transition requires.
486        required_values: u64,
487    },
488    /// Enrollment cannot add another live member claim.
489    LiveMemberClaimOverflow {
490        /// Current live member count.
491        live_members: u64,
492    },
493    /// Enrollment, detached attach, or fenced recovery cannot add a terminal claim.
494    BindingTerminalClaimOverflow {
495        /// Current binding-terminal claim count.
496        binding_terminals: u64,
497    },
498    /// The protocol-computed marker increment cannot be represented.
499    MarkerClaimOverflow {
500        /// Current marker claim count.
501        markers: u64,
502        /// New marker claims proposed by the fixed-point simulation.
503        new_markers: u64,
504    },
505    /// Fenced recovery was attempted without the coupled `RS=1, RT=1` reserve.
506    RecoverySequenceReserveMissing,
507    /// A fixed-point projection attempted to mint a second `RS`/`RT` pair.
508    RecoverySequenceReserveAlreadyPresent,
509    /// A reserved fenced-recovery transfer failed to preserve the sequence invariant.
510    RecoverySequenceInvariantViolation,
511}
512
513/// Applies the sealed resulting claim state to the sequence-reserve gate.
514///
515/// Exhaustion is decided from the complete resulting state, including every
516/// fixed-point marker and recovery claim. The refusal carries exactly the
517/// canonical ten-field [`SequenceBudget`] derived here.
518///
519/// # Errors
520///
521/// Returns [`SequenceAdmissionError::Exhausted`] if the canonical reserve sum
522/// overflows u128 or is greater than `u64::MAX - resulting_high_watermark`.
523pub fn admit_sequence(
524    request: SequenceAllocatingEnvelope,
525    resulting: ResultingSequenceState,
526) -> Result<SequenceAdmission, SequenceAdmissionError> {
527    let budget = resulting.claims.budget(resulting.high_watermark);
528    let Some(required_reserve) = checked_required_reserve(&budget) else {
529        return Err(SequenceAdmissionError::Exhausted(Box::new(
530            ConversationSequenceExhausted {
531                request,
532                sequence_budget: budget,
533            },
534        )));
535    };
536    if required_reserve > u128::from(budget.remaining) {
537        return Err(SequenceAdmissionError::Exhausted(Box::new(
538            ConversationSequenceExhausted {
539                request,
540                sequence_budget: budget,
541            },
542        )));
543    }
544
545    Ok(SequenceAdmission {
546        resulting: SequenceLedger {
547            high_watermark: resulting.high_watermark,
548            claims: resulting.claims,
549            required_reserve,
550        },
551    })
552}
553
554fn checked_required_reserve(budget: &SequenceBudget) -> Option<u128> {
555    let mut reserve = u128::from(budget.e);
556    reserve = reserve.checked_add(u128::from(budget.t))?;
557    reserve = reserve.checked_add(u128::from(budget.m))?;
558    reserve = reserve.checked_add(u128::from(budget.rs))?;
559    reserve = reserve.checked_add(u128::from(budget.rt))?;
560    reserve = reserve.checked_add(budget.l_times_t)?;
561    reserve = reserve.checked_add(budget.l_times_rt)?;
562    reserve.checked_add(budget.l_other_times_e)
563}