Skip to main content

liminal_protocol/lifecycle/operations/
binding_fate.rs

1use alloc::boxed::Box;
2
3use crate::{
4    algebra::{floor_transition, marker_clamped_floor},
5    wire::DeliverySeq,
6};
7
8use super::{LiveFrontierError, LiveFrontierOwner, live_frontier::BindingFateOwnerPlan};
9use crate::lifecycle::{
10    CommittedDiedTerminal, Event, FrontierBinding, ObserverProgressProjection, OrdinaryBindingFate,
11    RecoveredBindingFate, SealedBindingFateIntent, SealedBindingFateToken,
12};
13
14/// Closed terminal input accepted by protocol-owned binding-fate measurement.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum BindingFateTerminal {
17    /// Ordinary fate consumes the exact committed Died terminal.
18    Ordinary(CommittedDiedTerminal),
19    /// Recovered fate after committed Died receives no Died terminal.
20    Recovered,
21    /// Recovered fate after pending Died preserves exact finalizer authority.
22    RecoveredAndReserveFinalizer,
23}
24
25/// Protocol-produced binding fate after measuring the post-release floor.
26#[derive(Debug, PartialEq, Eq)]
27pub enum MeasuredBindingFate {
28    /// Ordinary no-marker fate with exact Died provenance.
29    Ordinary(OrdinaryBindingFate),
30    /// Fenced recovered fate with no Died terminal input.
31    Recovered(RecoveredBindingFate),
32}
33
34impl MeasuredBindingFate {
35    /// Returns the measured floor carried by either closed fate class.
36    #[must_use]
37    pub const fn resulting_floor(&self) -> DeliverySeq {
38        match self {
39            Self::Ordinary(fate) => fate.resulting_floor(),
40            Self::Recovered(fate) => fate.resulting_floor(),
41        }
42    }
43
44    /// Projects the protocol-measured floor for observer routing.
45    #[must_use]
46    pub const fn observer_progress_projection(&self) -> ObserverProgressProjection {
47        match self {
48            Self::Ordinary(fate) => fate.observer_progress_projection(),
49            Self::Recovered(fate) => fate.observer_progress_projection(),
50        }
51    }
52}
53
54/// Successful protocol measurement retaining the coupled frontier owner.
55#[derive(Debug, PartialEq, Eq)]
56pub struct PreparedBindingFate {
57    owner: LiveFrontierOwner,
58    fate: MeasuredBindingFate,
59    event: Event,
60}
61
62impl PreparedBindingFate {
63    /// Borrows the measured fate.
64    #[must_use]
65    pub const fn fate(&self) -> &MeasuredBindingFate {
66        &self.fate
67    }
68
69    /// Returns the internally minted binding-fate event.
70    #[must_use]
71    pub const fn event(&self) -> Event {
72        self.event
73    }
74
75    /// Consumes the prepared transition into the measured next owner and fate.
76    #[must_use]
77    pub fn into_parts(self) -> (LiveFrontierOwner, MeasuredBindingFate, Event) {
78        (self.owner, self.fate, self.event)
79    }
80}
81
82/// Floor authority measured before a Pending-Died enclosing finalizer commits.
83#[derive(Debug, PartialEq, Eq)]
84pub struct PendingDiedOrdinaryFinalizer {
85    resulting_floor: DeliverySeq,
86    authority: FinalizerAuthority,
87    provenance: Box<crate::lifecycle::storage::OrdinaryBindingFateRestore>,
88}
89
90#[derive(Debug, PartialEq, Eq)]
91struct FinalizerAuthority;
92
93/// Ordinary fate and unchanged owner prepared for an enclosing finalizer.
94#[derive(Debug, PartialEq, Eq)]
95pub struct PreparedPendingDiedOrdinaryFinalizer {
96    owner: LiveFrontierOwner,
97    fate: OrdinaryBindingFate,
98    finalizer: PendingDiedOrdinaryFinalizer,
99}
100
101impl PreparedPendingDiedOrdinaryFinalizer {
102    /// Returns the unchanged owner, measured fate, and one-use floor authority.
103    #[must_use]
104    pub fn into_parts(
105        self,
106    ) -> (
107        LiveFrontierOwner,
108        OrdinaryBindingFate,
109        PendingDiedOrdinaryFinalizer,
110    ) {
111        (self.owner, self.fate, self.finalizer)
112    }
113}
114
115impl LiveFrontierOwner {
116    /// Applies a measured Ordinary floor after its enclosing finalizer transition.
117    ///
118    /// # Errors
119    ///
120    /// Returns [`LiveFrontierError`] if retained charges, the measured floor,
121    /// marker precedence, or closure accounting disagree with the post-finalizer owner.
122    pub fn complete_pending_died_ordinary_finalizer(
123        self,
124        finalizer: PendingDiedOrdinaryFinalizer,
125    ) -> Result<Self, LiveFrontierError> {
126        let PendingDiedOrdinaryFinalizer {
127            resulting_floor,
128            authority,
129            provenance,
130        } = finalizer;
131        let FinalizerAuthority = authority;
132        let mut owner = self.install_finalized_binding_fate_floor(resulting_floor)?;
133        owner.retain_ordinary_cursor_release(provenance);
134        Ok(owner)
135    }
136}
137
138/// Typed reason protocol-owned binding-fate measurement refused.
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum BindingFateMeasurementError {
141    /// The sealed token has no unique ordinary or recovered authority.
142    Token,
143    /// The token names a different conversation.
144    Conversation,
145    /// The token's participant is absent from the coupled frontier.
146    Participant,
147    /// The token's binding epoch or cursor disagrees with the coupled frontier.
148    Binding,
149    /// Ordinary/recovered terminal input disagrees with the token class.
150    Terminal,
151    /// Hard observer progress exceeds the candidate high watermark.
152    ObserverProgress,
153    /// The measured checked floor is outside the delivery-sequence domain.
154    ResultingFloor,
155    /// The coupled frontier, retained charges, or closure baseline refused
156    /// installation, CARRYING the protocol's own cause.
157    ///
158    /// §3.3: the cause rides this carrier BY TYPE, because the park-versus-fatal
159    /// decision downstream is not allowed to read it back out of a formatted
160    /// message. The payload is free — both this enum and `LiveFrontierError`
161    /// are `Copy` (`live_frontier.rs:1047`), so the derives above survive it.
162    OwnerTransition(LiveFrontierError),
163}
164
165/// Refused measurement preserving every move-only input for serial retry.
166#[derive(Debug, PartialEq, Eq)]
167pub struct BindingFateMeasurementRefused {
168    owner: LiveFrontierOwner,
169    token: SealedBindingFateToken,
170    terminal: BindingFateTerminal,
171    error: BindingFateMeasurementError,
172}
173
174impl BindingFateMeasurementRefused {
175    /// Returns the typed refusal reason.
176    #[must_use]
177    pub const fn error(&self) -> BindingFateMeasurementError {
178        self.error
179    }
180
181    /// Recovers every unchanged input for a same-lock serial retry.
182    #[must_use]
183    pub fn into_parts(
184        self,
185    ) -> (
186        LiveFrontierOwner,
187        SealedBindingFateToken,
188        BindingFateTerminal,
189    ) {
190        (self.owner, self.token, self.terminal)
191    }
192}
193
194struct ValidatedBindingFateMeasurement {
195    participant_id: crate::wire::ParticipantId,
196    binding_epoch: crate::wire::BindingEpoch,
197    resulting_floor: DeliverySeq,
198    owner_plan: BindingFateOwnerPlan,
199}
200
201struct ValidatedBindingFateFloor {
202    participant_id: crate::wire::ParticipantId,
203    binding_epoch: crate::wire::BindingEpoch,
204    resulting_floor: DeliverySeq,
205}
206
207impl LiveFrontierOwner {
208    /// Consumes one sealed fate token after measuring its real post-release floor.
209    ///
210    /// The server supplies only hard observer progress and the closed terminal
211    /// class. The participant, binding epoch, current retained floor, candidate
212    /// high watermark, and remaining member cursors all come from protocol-owned
213    /// state. Recovered internally mints its event and accepts no terminal.
214    ///
215    /// # Errors
216    ///
217    /// Returns every input unchanged when authority, terminal class, observer
218    /// progress, or checked floor validation fails.
219    pub fn prepare_binding_fate(
220        self,
221        token: SealedBindingFateToken,
222        terminal: BindingFateTerminal,
223        hard_observer_progress: DeliverySeq,
224    ) -> Result<PreparedBindingFate, Box<BindingFateMeasurementRefused>> {
225        let measurement = match validate_binding_fate_measurement(
226            &self,
227            &token,
228            terminal,
229            hard_observer_progress,
230        ) {
231            Ok(measurement) => measurement,
232            Err(error) => return refusal(self, token, terminal, error),
233        };
234        let event = Event::binding_fate_observed(
235            measurement.participant_id,
236            measurement.binding_epoch,
237            measurement.resulting_floor,
238        );
239        let fate = match terminal {
240            BindingFateTerminal::Ordinary(terminal) => token
241                .ordinary_binding_fate(terminal, measurement.resulting_floor)
242                .map(MeasuredBindingFate::Ordinary),
243            BindingFateTerminal::Recovered | BindingFateTerminal::RecoveredAndReserveFinalizer => {
244                token
245                    .recovered_binding_fate_measured(measurement.resulting_floor)
246                    .map(MeasuredBindingFate::Recovered)
247            }
248        };
249        match fate {
250            Ok(fate) => {
251                let mut owner = self.install_binding_fate_transition(
252                    measurement.owner_plan,
253                    measurement.resulting_floor,
254                );
255                if let MeasuredBindingFate::Ordinary(source) = &fate {
256                    owner.retain_ordinary_cursor_release(Box::new(source.storage_description()));
257                }
258                Ok(PreparedBindingFate { owner, fate, event })
259            }
260            Err(token) => Err(Box::new(BindingFateMeasurementRefused {
261                owner: self,
262                token: *token,
263                terminal,
264                error: BindingFateMeasurementError::Terminal,
265            })),
266        }
267    }
268
269    /// Measures Ordinary without installing its owner transition before an enclosing finalizer.
270    ///
271    /// # Errors
272    ///
273    /// Returns every move-only input unchanged when the token, terminal,
274    /// observer progress, measured floor, or pre-finalizer owner transition
275    /// is inconsistent.
276    pub fn prepare_pending_died_ordinary_finalizer(
277        self,
278        token: SealedBindingFateToken,
279        terminal: CommittedDiedTerminal,
280        hard_observer_progress: DeliverySeq,
281    ) -> Result<PreparedPendingDiedOrdinaryFinalizer, Box<BindingFateMeasurementRefused>> {
282        let terminal_input = BindingFateTerminal::Ordinary(terminal);
283        let measurement = match validate_binding_fate_measurement(
284            &self,
285            &token,
286            terminal_input,
287            hard_observer_progress,
288        ) {
289            Ok(measurement) => measurement,
290            Err(error) => {
291                return Err(Box::new(BindingFateMeasurementRefused {
292                    owner: self,
293                    token,
294                    terminal: terminal_input,
295                    error,
296                }));
297            }
298        };
299        let resulting_floor = measurement.resulting_floor;
300        match token.ordinary_binding_fate(terminal, resulting_floor) {
301            Ok(fate) => Ok(PreparedPendingDiedOrdinaryFinalizer {
302                owner: self,
303                fate,
304                finalizer: PendingDiedOrdinaryFinalizer {
305                    resulting_floor,
306                    authority: FinalizerAuthority,
307                    provenance: Box::new(fate.storage_description()),
308                },
309            }),
310            Err(token) => Err(Box::new(BindingFateMeasurementRefused {
311                owner: self,
312                token: *token,
313                terminal: terminal_input,
314                error: BindingFateMeasurementError::Terminal,
315            })),
316        }
317    }
318
319    /// Measures Ordinary after fenced proof minting consumed marker authority.
320    ///
321    /// The fenced attach transition itself owns the pending identity change, so
322    /// this preparation validates the token, terminal, cursor, and exact floor
323    /// but defers floor installation to the post-attach owner.
324    ///
325    /// # Errors
326    ///
327    /// Returns every move-only input unchanged when token authority, terminal,
328    /// observer progress, or checked floor measurement is inconsistent.
329    pub fn prepare_pending_died_ordinary_after_fenced_proof(
330        self,
331        token: SealedBindingFateToken,
332        terminal: CommittedDiedTerminal,
333        hard_observer_progress: DeliverySeq,
334    ) -> Result<PreparedPendingDiedOrdinaryFinalizer, Box<BindingFateMeasurementRefused>> {
335        let terminal_input = BindingFateTerminal::Ordinary(terminal);
336        let measurement = match validate_binding_fate_floor(
337            &self,
338            &token,
339            terminal_input,
340            hard_observer_progress,
341        ) {
342            Ok(measurement) => measurement,
343            Err(error) => {
344                return Err(Box::new(BindingFateMeasurementRefused {
345                    owner: self,
346                    token,
347                    terminal: terminal_input,
348                    error,
349                }));
350            }
351        };
352        let resulting_floor = measurement.resulting_floor;
353        match token.ordinary_binding_fate(terminal, resulting_floor) {
354            Ok(fate) => Ok(PreparedPendingDiedOrdinaryFinalizer {
355                owner: self,
356                fate,
357                finalizer: PendingDiedOrdinaryFinalizer {
358                    resulting_floor,
359                    authority: FinalizerAuthority,
360                    provenance: Box::new(fate.storage_description()),
361                },
362            }),
363            Err(token) => Err(Box::new(BindingFateMeasurementRefused {
364                owner: self,
365                token: *token,
366                terminal: terminal_input,
367                error: BindingFateMeasurementError::Terminal,
368            })),
369        }
370    }
371}
372
373fn validate_binding_fate_measurement(
374    owner: &LiveFrontierOwner,
375    token: &SealedBindingFateToken,
376    terminal: BindingFateTerminal,
377    hard_observer_progress: DeliverySeq,
378) -> Result<ValidatedBindingFateMeasurement, BindingFateMeasurementError> {
379    let floor = validate_binding_fate_floor(owner, token, terminal, hard_observer_progress)?;
380    let owner_plan = owner
381        .prepare_binding_fate_transition(
382            floor.participant_id,
383            floor.binding_epoch,
384            token
385                .measurement_context()
386                .ok_or(BindingFateMeasurementError::Token)?
387                .cursor,
388            floor.resulting_floor,
389            terminal == BindingFateTerminal::RecoveredAndReserveFinalizer,
390        )
391        .map_err(BindingFateMeasurementError::OwnerTransition)?;
392    Ok(ValidatedBindingFateMeasurement {
393        participant_id: floor.participant_id,
394        binding_epoch: floor.binding_epoch,
395        resulting_floor: floor.resulting_floor,
396        owner_plan,
397    })
398}
399
400fn validate_binding_fate_floor(
401    owner: &LiveFrontierOwner,
402    token: &SealedBindingFateToken,
403    terminal: BindingFateTerminal,
404    hard_observer_progress: DeliverySeq,
405) -> Result<ValidatedBindingFateFloor, BindingFateMeasurementError> {
406    let Some(context) = token.measurement_context() else {
407        return Err(BindingFateMeasurementError::Token);
408    };
409    if context.conversation_id != owner.frontiers().conversation_id() {
410        return Err(BindingFateMeasurementError::Conversation);
411    }
412    let Some(participant) = owner
413        .frontiers()
414        .active_identities()
415        .participants()
416        .iter()
417        .find(|participant| participant.participant_index() == context.participant_id)
418    else {
419        return Err(BindingFateMeasurementError::Participant);
420    };
421    if participant.cursor() != context.cursor
422        || participant.binding() != FrontierBinding::Bound(context.binding_epoch)
423            && participant.binding() != FrontierBinding::Detached(context.binding_epoch)
424    {
425        return Err(BindingFateMeasurementError::Binding);
426    }
427    let terminal_matches = match (token.intent(), terminal) {
428        (Some(SealedBindingFateIntent::Ordinary), BindingFateTerminal::Ordinary(died)) => {
429            died.conversation_id() == context.conversation_id
430                && died.participant_id() == context.participant_id
431                && died.binding_epoch() == context.binding_epoch
432        }
433        (
434            Some(SealedBindingFateIntent::Recovered { .. }),
435            BindingFateTerminal::Recovered | BindingFateTerminal::RecoveredAndReserveFinalizer,
436        ) => true,
437        _ => false,
438    };
439    if !terminal_matches {
440        return Err(BindingFateMeasurementError::Terminal);
441    }
442    let candidate_high_watermark = owner.frontiers().sequence().ledger().high_watermark();
443    if hard_observer_progress > candidate_high_watermark {
444        return Err(BindingFateMeasurementError::ObserverProgress);
445    }
446    let minimum_remaining_cursor = owner
447        .frontiers()
448        .active_identities()
449        .participants()
450        .iter()
451        .filter(|participant| participant.participant_index() != context.participant_id)
452        .map(|participant| participant.cursor())
453        .min();
454    let measured = floor_transition(
455        owner.frontiers().retained_floor(),
456        minimum_remaining_cursor,
457        candidate_high_watermark,
458        hard_observer_progress,
459        owner.frontiers().retained_floor(),
460    );
461    // §3.1 MARKERS PIN THE FLOOR (docs/design/F8-MARKER-POISON.md §3.1).
462    //
463    // `floor_transition` above takes no marker input of any kind -- its five
464    // arguments are the retained floor, the minimum remaining cursor, the
465    // candidate high watermark, hard observer progress, and the retained floor
466    // again. Downstream, the live-frontier transition REFUSES a floor that
467    // crosses a retained marker (`LiveFrontierError::Precedence`). Those were
468    // two halves of one invariant with neither clamping, so the computation
469    // could produce a floor the transition was obliged to reject: the incident
470    // in §1, where the refusal WAS collapsed to a bare `OwnerTransition` and
471    // the Died row carrying the intent WAS already durable by the time the
472    // measurement ran. Both halves of that clause describe the PRE-FIX path and
473    // nothing else. §3.3 now carries the cause by type (`OwnerTransition` takes
474    // a `LiveFrontierError`), and §3.2 split the live path so the measurement
475    // runs BEFORE the source append -- only the combined boot form still
476    // measures against a source that is already durable.
477    //
478    // The purpose of a retained marker is precisely that its record stays
479    // replayable until acked. Pinning is the marker's meaning; unsatisfiability
480    // was the bug. So the measured floor is LOWERED to the minimum retained
481    // marker-record sequence and can no longer cross one.
482    //
483    // NOT `cap_floor` (PRECEDENCE-CLAMP M2). `floor_transition`'s fifth argument
484    // is named `cap_floor` and is `max(base_result, cap_floor)` — a floor
485    // RAISER. This clamp LOWERS. Routing the lowest marker through `cap_floor`
486    // yields `max(base, marker)`, which still sits above the marker in exactly
487    // the poisoning case and would raise floors in cases that work today. The
488    // clamp therefore has its own named primitive, `marker_clamped_floor`, and
489    // never travels through `cap_floor`.
490    //
491    // The `Precedence` refusal STAYS as a backstop invariant. After this clamp
492    // it should be unreachable from this path, and a reachable backstop firing
493    // is a bug report rather than control flow.
494    //
495    // WHY A CLAMP BESIDE `search_capacity_floor` AND NOT A REUSE OF IT
496    // (PRECEDENCE-CLAMP M3, decided rather than guessed).
497    // `ordinary_record_projection.rs`'s `search_capacity_floor` does the
498    // structurally similar thing for its own invariant, and it is the wrong
499    // tool here for three independent reasons, any one of which is fatal:
500    //   1. DIRECTION. It SEARCHES UPWARD, walking the floor to successively
501    //      higher retained sequences until a capacity/credit baseline admits,
502    //      and it feeds its answer back through `floor_transition`'s `cap_floor`
503    //      — a raiser. Precedence needs the floor LOWERED.
504    //   2. INVARIANT. Its subject is capacity: whether the resulting retained
505    //      charge fits the configured cap. Precedence's subject is replayability:
506    //      an unacked marker must stay retained. It treats a marker anchor below
507    //      its chosen floor as an ERROR (`MarkerAnchorCapacity`) rather than as a
508    //      bound, which is the opposite of pinning.
509    //   3. INPUTS. It is a function of `OrdinaryProjectionFacts` — an order
510    //      ledger, a sequence ledger, an admission request, encoded charges —
511    //      none of which exist at a binding-fate measurement.
512    // The two are NOT interchangeable, so the marker rule gets its own named
513    // primitive, `marker_clamped_floor`, and the two live side by side.
514    let lowest_retained_marker_seq = owner
515        .frontiers()
516        .retained_marker_records()
517        .iter()
518        .map(|record| record.delivery_seq)
519        .min();
520    let clamped_floor = marker_clamped_floor(measured.resulting_floor, lowest_retained_marker_seq);
521    let Ok(resulting_floor) = DeliverySeq::try_from(clamped_floor) else {
522        return Err(BindingFateMeasurementError::ResultingFloor);
523    };
524    Ok(ValidatedBindingFateFloor {
525        participant_id: context.participant_id,
526        binding_epoch: context.binding_epoch,
527        resulting_floor,
528    })
529}
530
531fn refusal(
532    owner: LiveFrontierOwner,
533    token: SealedBindingFateToken,
534    terminal: BindingFateTerminal,
535    error: BindingFateMeasurementError,
536) -> Result<PreparedBindingFate, Box<BindingFateMeasurementRefused>> {
537    Err(Box::new(BindingFateMeasurementRefused {
538        owner,
539        token,
540        terminal,
541        error,
542    }))
543}
544
545impl PendingDiedOrdinaryFinalizer {
546    /// Borrows the exact already-measured finalization floor into canonical bytes.
547    #[must_use]
548    pub const fn encode_checkpoint(&self) -> [u8; 8] {
549        self.resulting_floor.to_be_bytes()
550    }
551
552    /// Restores only the finalizer belonging to a validated ordinary death fate.
553    ///
554    /// The live owner's existing completion checks still govern floor installation.
555    ///
556    /// # Errors
557    ///
558    /// Refuses an incorrect width or a floor different from the validated fate.
559    pub fn restore_checkpoint(
560        body: &[u8],
561        fate: &OrdinaryBindingFate,
562    ) -> Result<Self, crate::lifecycle::OwnerCheckpointRestoreError> {
563        let bytes: [u8; 8] = body.try_into().map_err(|_| {
564            crate::lifecycle::OwnerCheckpointRestoreError::Field("pending_died_finalizer.framing")
565        })?;
566        let resulting_floor = u64::from_be_bytes(bytes);
567        if resulting_floor != fate.resulting_floor() {
568            return Err(crate::lifecycle::OwnerCheckpointRestoreError::Field(
569                "pending_died_finalizer.floor",
570            ));
571        }
572        Ok(Self {
573            resulting_floor,
574            authority: FinalizerAuthority,
575            provenance: Box::new(fate.storage_description()),
576        })
577    }
578}