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    fn checked_high_watermark(self, required_values: u64) -> Result<u64, SequenceAdmissionError> {
403        self.high_watermark.checked_add(required_values).ok_or(
404            SequenceAdmissionError::HighWatermarkOverflow {
405                high_watermark: self.high_watermark,
406                required_values,
407            },
408        )
409    }
410
411    fn checked_markers(self, new_markers: u64) -> Result<u64, SequenceAdmissionError> {
412        self.claims.markers.checked_add(new_markers).ok_or(
413            SequenceAdmissionError::MarkerClaimOverflow {
414                markers: self.claims.markers,
415                new_markers,
416            },
417        )
418    }
419}
420
421/// Protocol-produced proposed sequence state.
422///
423/// Construction is crate-private so a consuming server cannot invent the
424/// post-operation `L`, `T`, `M`, `RS`, or `RT` state used by admission.
425#[derive(Clone, Copy, Debug, PartialEq, Eq)]
426pub struct ResultingSequenceState {
427    high_watermark: u64,
428    claims: SequenceClaims,
429}
430
431/// Successful sequence-reserve admission.
432#[derive(Clone, Copy, Debug, PartialEq, Eq)]
433pub struct SequenceAdmission {
434    resulting: SequenceLedger,
435}
436
437impl SequenceAdmission {
438    /// Returns the complete validated proposed ledger.
439    #[must_use]
440    pub const fn resulting(self) -> SequenceLedger {
441        self.resulting
442    }
443}
444
445/// Sequence admission refusal.
446#[derive(Clone, Debug, PartialEq, Eq)]
447pub enum SequenceAdmissionError {
448    /// The proposed canonical reserve exceeds the resulting sequence suffix.
449    Exhausted(Box<ConversationSequenceExhausted>),
450    /// Appending the operation's record count would exceed the sequence domain.
451    HighWatermarkOverflow {
452        /// Current high watermark.
453        high_watermark: u64,
454        /// Number of consecutive values the transition requires.
455        required_values: u64,
456    },
457    /// Enrollment cannot add another live member claim.
458    LiveMemberClaimOverflow {
459        /// Current live member count.
460        live_members: u64,
461    },
462    /// Enrollment, detached attach, or fenced recovery cannot add a terminal claim.
463    BindingTerminalClaimOverflow {
464        /// Current binding-terminal claim count.
465        binding_terminals: u64,
466    },
467    /// The protocol-computed marker increment cannot be represented.
468    MarkerClaimOverflow {
469        /// Current marker claim count.
470        markers: u64,
471        /// New marker claims proposed by the fixed-point simulation.
472        new_markers: u64,
473    },
474    /// Fenced recovery was attempted without the coupled `RS=1, RT=1` reserve.
475    RecoverySequenceReserveMissing,
476    /// A fixed-point projection attempted to mint a second `RS`/`RT` pair.
477    RecoverySequenceReserveAlreadyPresent,
478    /// A reserved fenced-recovery transfer failed to preserve the sequence invariant.
479    RecoverySequenceInvariantViolation,
480}
481
482/// Applies the sealed resulting claim state to the sequence-reserve gate.
483///
484/// Exhaustion is decided from the complete resulting state, including every
485/// fixed-point marker and recovery claim. The refusal carries exactly the
486/// canonical ten-field [`SequenceBudget`] derived here.
487///
488/// # Errors
489///
490/// Returns [`SequenceAdmissionError::Exhausted`] if the canonical reserve sum
491/// overflows u128 or is greater than `u64::MAX - resulting_high_watermark`.
492pub fn admit_sequence(
493    request: SequenceAllocatingEnvelope,
494    resulting: ResultingSequenceState,
495) -> Result<SequenceAdmission, SequenceAdmissionError> {
496    let budget = resulting.claims.budget(resulting.high_watermark);
497    let Some(required_reserve) = checked_required_reserve(&budget) else {
498        return Err(SequenceAdmissionError::Exhausted(Box::new(
499            ConversationSequenceExhausted {
500                request,
501                sequence_budget: budget,
502            },
503        )));
504    };
505    if required_reserve > u128::from(budget.remaining) {
506        return Err(SequenceAdmissionError::Exhausted(Box::new(
507            ConversationSequenceExhausted {
508                request,
509                sequence_budget: budget,
510            },
511        )));
512    }
513
514    Ok(SequenceAdmission {
515        resulting: SequenceLedger {
516            high_watermark: resulting.high_watermark,
517            claims: resulting.claims,
518            required_reserve,
519        },
520    })
521}
522
523fn checked_required_reserve(budget: &SequenceBudget) -> Option<u128> {
524    let mut reserve = u128::from(budget.e);
525    reserve = reserve.checked_add(u128::from(budget.t))?;
526    reserve = reserve.checked_add(u128::from(budget.m))?;
527    reserve = reserve.checked_add(u128::from(budget.rs))?;
528    reserve = reserve.checked_add(u128::from(budget.rt))?;
529    reserve = reserve.checked_add(budget.l_times_t)?;
530    reserve = reserve.checked_add(budget.l_times_rt)?;
531    reserve.checked_add(budget.l_other_times_e)
532}