Skip to main content

liminal_protocol/lifecycle/operations/
binding_fate.rs

1use alloc::boxed::Box;
2
3use crate::{algebra::floor_transition, wire::DeliverySeq};
4
5use super::{LiveFrontierError, LiveFrontierOwner, live_frontier::BindingFateOwnerPlan};
6use crate::lifecycle::{
7    CommittedDiedTerminal, Event, FrontierBinding, ObserverProgressProjection, OrdinaryBindingFate,
8    RecoveredBindingFate, SealedBindingFateIntent, SealedBindingFateToken,
9};
10
11/// Closed terminal input accepted by protocol-owned binding-fate measurement.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum BindingFateTerminal {
14    /// Ordinary fate consumes the exact committed Died terminal.
15    Ordinary(CommittedDiedTerminal),
16    /// Recovered fate after committed Died receives no Died terminal.
17    Recovered,
18    /// Recovered fate after pending Died preserves exact finalizer authority.
19    RecoveredAndReserveFinalizer,
20}
21
22/// Protocol-produced binding fate after measuring the post-release floor.
23#[derive(Debug, PartialEq, Eq)]
24pub enum MeasuredBindingFate {
25    /// Ordinary no-marker fate with exact Died provenance.
26    Ordinary(OrdinaryBindingFate),
27    /// Fenced recovered fate with no Died terminal input.
28    Recovered(RecoveredBindingFate),
29}
30
31impl MeasuredBindingFate {
32    /// Returns the measured floor carried by either closed fate class.
33    #[must_use]
34    pub const fn resulting_floor(&self) -> DeliverySeq {
35        match self {
36            Self::Ordinary(fate) => fate.resulting_floor(),
37            Self::Recovered(fate) => fate.resulting_floor(),
38        }
39    }
40
41    /// Projects the protocol-measured floor for observer routing.
42    #[must_use]
43    pub const fn observer_progress_projection(&self) -> ObserverProgressProjection {
44        match self {
45            Self::Ordinary(fate) => fate.observer_progress_projection(),
46            Self::Recovered(fate) => fate.observer_progress_projection(),
47        }
48    }
49}
50
51/// Successful protocol measurement retaining the coupled frontier owner.
52#[derive(Debug, PartialEq, Eq)]
53pub struct PreparedBindingFate {
54    owner: LiveFrontierOwner,
55    fate: MeasuredBindingFate,
56    event: Event,
57}
58
59impl PreparedBindingFate {
60    /// Borrows the measured fate.
61    #[must_use]
62    pub const fn fate(&self) -> &MeasuredBindingFate {
63        &self.fate
64    }
65
66    /// Returns the internally minted binding-fate event.
67    #[must_use]
68    pub const fn event(&self) -> Event {
69        self.event
70    }
71
72    /// Consumes the prepared transition into the measured next owner and fate.
73    #[must_use]
74    pub fn into_parts(self) -> (LiveFrontierOwner, MeasuredBindingFate, Event) {
75        (self.owner, self.fate, self.event)
76    }
77}
78
79/// Floor authority measured before a Pending-Died enclosing finalizer commits.
80#[derive(Debug, PartialEq, Eq)]
81pub struct PendingDiedOrdinaryFinalizer {
82    resulting_floor: DeliverySeq,
83    authority: FinalizerAuthority,
84}
85
86#[derive(Debug, PartialEq, Eq)]
87struct FinalizerAuthority;
88
89/// Ordinary fate and unchanged owner prepared for an enclosing finalizer.
90#[derive(Debug, PartialEq, Eq)]
91pub struct PreparedPendingDiedOrdinaryFinalizer {
92    owner: LiveFrontierOwner,
93    fate: OrdinaryBindingFate,
94    finalizer: PendingDiedOrdinaryFinalizer,
95}
96
97impl PreparedPendingDiedOrdinaryFinalizer {
98    /// Returns the unchanged owner, measured fate, and one-use floor authority.
99    #[must_use]
100    pub fn into_parts(
101        self,
102    ) -> (
103        LiveFrontierOwner,
104        OrdinaryBindingFate,
105        PendingDiedOrdinaryFinalizer,
106    ) {
107        (self.owner, self.fate, self.finalizer)
108    }
109}
110
111impl LiveFrontierOwner {
112    /// Applies a measured Ordinary floor after its enclosing finalizer transition.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`LiveFrontierError`] if retained charges, the measured floor,
117    /// marker precedence, or closure accounting disagree with the post-finalizer owner.
118    pub fn complete_pending_died_ordinary_finalizer(
119        self,
120        finalizer: PendingDiedOrdinaryFinalizer,
121    ) -> Result<Self, LiveFrontierError> {
122        let PendingDiedOrdinaryFinalizer {
123            resulting_floor,
124            authority,
125        } = finalizer;
126        let FinalizerAuthority = authority;
127        self.install_finalized_binding_fate_floor(resulting_floor)
128    }
129}
130
131/// Typed reason protocol-owned binding-fate measurement refused.
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub enum BindingFateMeasurementError {
134    /// The sealed token has no unique ordinary or recovered authority.
135    Token,
136    /// The token names a different conversation.
137    Conversation,
138    /// The token's participant is absent from the coupled frontier.
139    Participant,
140    /// The token's binding epoch or cursor disagrees with the coupled frontier.
141    Binding,
142    /// Ordinary/recovered terminal input disagrees with the token class.
143    Terminal,
144    /// Hard observer progress exceeds the candidate high watermark.
145    ObserverProgress,
146    /// The measured checked floor is outside the delivery-sequence domain.
147    ResultingFloor,
148    /// The coupled frontier, retained charges, or closure baseline refused installation.
149    OwnerTransition,
150}
151
152/// Refused measurement preserving every move-only input for serial retry.
153#[derive(Debug, PartialEq, Eq)]
154pub struct BindingFateMeasurementRefused {
155    owner: LiveFrontierOwner,
156    token: SealedBindingFateToken,
157    terminal: BindingFateTerminal,
158    error: BindingFateMeasurementError,
159}
160
161impl BindingFateMeasurementRefused {
162    /// Returns the typed refusal reason.
163    #[must_use]
164    pub const fn error(&self) -> BindingFateMeasurementError {
165        self.error
166    }
167
168    /// Recovers every unchanged input for a same-lock serial retry.
169    #[must_use]
170    pub fn into_parts(
171        self,
172    ) -> (
173        LiveFrontierOwner,
174        SealedBindingFateToken,
175        BindingFateTerminal,
176    ) {
177        (self.owner, self.token, self.terminal)
178    }
179}
180
181struct ValidatedBindingFateMeasurement {
182    participant_id: crate::wire::ParticipantId,
183    binding_epoch: crate::wire::BindingEpoch,
184    resulting_floor: DeliverySeq,
185    owner_plan: BindingFateOwnerPlan,
186}
187
188struct ValidatedBindingFateFloor {
189    participant_id: crate::wire::ParticipantId,
190    binding_epoch: crate::wire::BindingEpoch,
191    resulting_floor: DeliverySeq,
192}
193
194impl LiveFrontierOwner {
195    /// Consumes one sealed fate token after measuring its real post-release floor.
196    ///
197    /// The server supplies only hard observer progress and the closed terminal
198    /// class. The participant, binding epoch, current retained floor, candidate
199    /// high watermark, and remaining member cursors all come from protocol-owned
200    /// state. Recovered internally mints its event and accepts no terminal.
201    ///
202    /// # Errors
203    ///
204    /// Returns every input unchanged when authority, terminal class, observer
205    /// progress, or checked floor validation fails.
206    pub fn prepare_binding_fate(
207        self,
208        token: SealedBindingFateToken,
209        terminal: BindingFateTerminal,
210        hard_observer_progress: DeliverySeq,
211    ) -> Result<PreparedBindingFate, Box<BindingFateMeasurementRefused>> {
212        let measurement = match validate_binding_fate_measurement(
213            &self,
214            &token,
215            terminal,
216            hard_observer_progress,
217        ) {
218            Ok(measurement) => measurement,
219            Err(error) => return refusal(self, token, terminal, error),
220        };
221        let event = Event::binding_fate_observed(
222            measurement.participant_id,
223            measurement.binding_epoch,
224            measurement.resulting_floor,
225        );
226        let fate = match terminal {
227            BindingFateTerminal::Ordinary(terminal) => token
228                .ordinary_binding_fate(terminal, measurement.resulting_floor)
229                .map(MeasuredBindingFate::Ordinary),
230            BindingFateTerminal::Recovered | BindingFateTerminal::RecoveredAndReserveFinalizer => {
231                token
232                    .recovered_binding_fate_measured(measurement.resulting_floor)
233                    .map(MeasuredBindingFate::Recovered)
234            }
235        };
236        match fate {
237            Ok(fate) => {
238                let owner = self.install_binding_fate_transition(
239                    measurement.owner_plan,
240                    measurement.resulting_floor,
241                );
242                Ok(PreparedBindingFate { owner, fate, event })
243            }
244            Err(token) => Err(Box::new(BindingFateMeasurementRefused {
245                owner: self,
246                token: *token,
247                terminal,
248                error: BindingFateMeasurementError::Terminal,
249            })),
250        }
251    }
252
253    /// Measures Ordinary without installing its owner transition before an enclosing finalizer.
254    ///
255    /// # Errors
256    ///
257    /// Returns every move-only input unchanged when the token, terminal,
258    /// observer progress, measured floor, or pre-finalizer owner transition
259    /// is inconsistent.
260    pub fn prepare_pending_died_ordinary_finalizer(
261        self,
262        token: SealedBindingFateToken,
263        terminal: CommittedDiedTerminal,
264        hard_observer_progress: DeliverySeq,
265    ) -> Result<PreparedPendingDiedOrdinaryFinalizer, Box<BindingFateMeasurementRefused>> {
266        let terminal_input = BindingFateTerminal::Ordinary(terminal);
267        let measurement = match validate_binding_fate_measurement(
268            &self,
269            &token,
270            terminal_input,
271            hard_observer_progress,
272        ) {
273            Ok(measurement) => measurement,
274            Err(error) => {
275                return Err(Box::new(BindingFateMeasurementRefused {
276                    owner: self,
277                    token,
278                    terminal: terminal_input,
279                    error,
280                }));
281            }
282        };
283        let resulting_floor = measurement.resulting_floor;
284        match token.ordinary_binding_fate(terminal, resulting_floor) {
285            Ok(fate) => Ok(PreparedPendingDiedOrdinaryFinalizer {
286                owner: self,
287                fate,
288                finalizer: PendingDiedOrdinaryFinalizer {
289                    resulting_floor,
290                    authority: FinalizerAuthority,
291                },
292            }),
293            Err(token) => Err(Box::new(BindingFateMeasurementRefused {
294                owner: self,
295                token: *token,
296                terminal: terminal_input,
297                error: BindingFateMeasurementError::Terminal,
298            })),
299        }
300    }
301
302    /// Measures Ordinary after fenced proof minting consumed marker authority.
303    ///
304    /// The fenced attach transition itself owns the pending identity change, so
305    /// this preparation validates the token, terminal, cursor, and exact floor
306    /// but defers floor installation to the post-attach owner.
307    ///
308    /// # Errors
309    ///
310    /// Returns every move-only input unchanged when token authority, terminal,
311    /// observer progress, or checked floor measurement is inconsistent.
312    pub fn prepare_pending_died_ordinary_after_fenced_proof(
313        self,
314        token: SealedBindingFateToken,
315        terminal: CommittedDiedTerminal,
316        hard_observer_progress: DeliverySeq,
317    ) -> Result<PreparedPendingDiedOrdinaryFinalizer, Box<BindingFateMeasurementRefused>> {
318        let terminal_input = BindingFateTerminal::Ordinary(terminal);
319        let measurement = match validate_binding_fate_floor(
320            &self,
321            &token,
322            terminal_input,
323            hard_observer_progress,
324        ) {
325            Ok(measurement) => measurement,
326            Err(error) => {
327                return Err(Box::new(BindingFateMeasurementRefused {
328                    owner: self,
329                    token,
330                    terminal: terminal_input,
331                    error,
332                }));
333            }
334        };
335        let resulting_floor = measurement.resulting_floor;
336        match token.ordinary_binding_fate(terminal, resulting_floor) {
337            Ok(fate) => Ok(PreparedPendingDiedOrdinaryFinalizer {
338                owner: self,
339                fate,
340                finalizer: PendingDiedOrdinaryFinalizer {
341                    resulting_floor,
342                    authority: FinalizerAuthority,
343                },
344            }),
345            Err(token) => Err(Box::new(BindingFateMeasurementRefused {
346                owner: self,
347                token: *token,
348                terminal: terminal_input,
349                error: BindingFateMeasurementError::Terminal,
350            })),
351        }
352    }
353}
354
355fn validate_binding_fate_measurement(
356    owner: &LiveFrontierOwner,
357    token: &SealedBindingFateToken,
358    terminal: BindingFateTerminal,
359    hard_observer_progress: DeliverySeq,
360) -> Result<ValidatedBindingFateMeasurement, BindingFateMeasurementError> {
361    let floor = validate_binding_fate_floor(owner, token, terminal, hard_observer_progress)?;
362    let owner_plan = owner
363        .prepare_binding_fate_transition(
364            floor.participant_id,
365            floor.binding_epoch,
366            token
367                .measurement_context()
368                .ok_or(BindingFateMeasurementError::Token)?
369                .cursor,
370            floor.resulting_floor,
371            terminal == BindingFateTerminal::RecoveredAndReserveFinalizer,
372        )
373        .map_err(|_| BindingFateMeasurementError::OwnerTransition)?;
374    Ok(ValidatedBindingFateMeasurement {
375        participant_id: floor.participant_id,
376        binding_epoch: floor.binding_epoch,
377        resulting_floor: floor.resulting_floor,
378        owner_plan,
379    })
380}
381
382fn validate_binding_fate_floor(
383    owner: &LiveFrontierOwner,
384    token: &SealedBindingFateToken,
385    terminal: BindingFateTerminal,
386    hard_observer_progress: DeliverySeq,
387) -> Result<ValidatedBindingFateFloor, BindingFateMeasurementError> {
388    let Some(context) = token.measurement_context() else {
389        return Err(BindingFateMeasurementError::Token);
390    };
391    if context.conversation_id != owner.frontiers().conversation_id() {
392        return Err(BindingFateMeasurementError::Conversation);
393    }
394    let Some(participant) = owner
395        .frontiers()
396        .active_identities()
397        .participants()
398        .iter()
399        .find(|participant| participant.participant_index() == context.participant_id)
400    else {
401        return Err(BindingFateMeasurementError::Participant);
402    };
403    if participant.cursor() != context.cursor
404        || participant.binding() != FrontierBinding::Bound(context.binding_epoch)
405            && participant.binding() != FrontierBinding::Detached(context.binding_epoch)
406    {
407        return Err(BindingFateMeasurementError::Binding);
408    }
409    let terminal_matches = match (token.intent(), terminal) {
410        (Some(SealedBindingFateIntent::Ordinary), BindingFateTerminal::Ordinary(died)) => {
411            died.conversation_id() == context.conversation_id
412                && died.participant_id() == context.participant_id
413                && died.binding_epoch() == context.binding_epoch
414        }
415        (
416            Some(SealedBindingFateIntent::Recovered { .. }),
417            BindingFateTerminal::Recovered | BindingFateTerminal::RecoveredAndReserveFinalizer,
418        ) => true,
419        _ => false,
420    };
421    if !terminal_matches {
422        return Err(BindingFateMeasurementError::Terminal);
423    }
424    let candidate_high_watermark = owner.frontiers().sequence().ledger().high_watermark();
425    if hard_observer_progress > candidate_high_watermark {
426        return Err(BindingFateMeasurementError::ObserverProgress);
427    }
428    let minimum_remaining_cursor = owner
429        .frontiers()
430        .active_identities()
431        .participants()
432        .iter()
433        .filter(|participant| participant.participant_index() != context.participant_id)
434        .map(|participant| participant.cursor())
435        .min();
436    let measured = floor_transition(
437        owner.frontiers().retained_floor(),
438        minimum_remaining_cursor,
439        candidate_high_watermark,
440        hard_observer_progress,
441        owner.frontiers().retained_floor(),
442    );
443    let Ok(resulting_floor) = DeliverySeq::try_from(measured.resulting_floor) else {
444        return Err(BindingFateMeasurementError::ResultingFloor);
445    };
446    Ok(ValidatedBindingFateFloor {
447        participant_id: context.participant_id,
448        binding_epoch: context.binding_epoch,
449        resulting_floor,
450    })
451}
452
453fn refusal(
454    owner: LiveFrontierOwner,
455    token: SealedBindingFateToken,
456    terminal: BindingFateTerminal,
457    error: BindingFateMeasurementError,
458) -> Result<PreparedBindingFate, Box<BindingFateMeasurementRefused>> {
459    Err(Box::new(BindingFateMeasurementRefused {
460        owner,
461        token,
462        terminal,
463        error,
464    }))
465}