Skip to main content

liminal_protocol/lifecycle/
binding_fate.rs

1use alloc::boxed::Box;
2
3use crate::wire::{BindingEpoch, ConversationId, DeliverySeq, ParticipantId};
4
5#[cfg(test)]
6use super::FencedAttachCommit;
7use super::{
8    CommittedDiedTerminal, Event, OrdinaryBindingFate, RecoveredBindingFate, SealedBindingFateToken,
9};
10
11/// Closed persistence shape carried by one sealed binding-fate token.
12///
13/// This projection exposes only the durable intent fields. It neither exposes
14/// nor duplicates the move-only authority consumed by protocol measurement.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum SealedBindingFateIntent {
17    /// A no-marker attachment must complete through an exact Died terminal.
18    Ordinary,
19    /// A fenced attachment retains the exact prior epoch and accepted marker.
20    Recovered {
21        /// Binding epoch whose marker authorized the fenced replacement.
22        prior_binding_epoch: BindingEpoch,
23        /// Exact accepted marker delivery sequence.
24        marker_delivery_seq: DeliverySeq,
25    },
26}
27
28/// Protocol-private measurement inputs carried by one sealed fate token.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub(in crate::lifecycle) struct BindingFateMeasurementContext {
31    pub(in crate::lifecycle) conversation_id: ConversationId,
32    pub(in crate::lifecycle) participant_id: ParticipantId,
33    pub(in crate::lifecycle) binding_epoch: BindingEpoch,
34    pub(in crate::lifecycle) cursor: DeliverySeq,
35}
36
37impl SealedBindingFateToken {
38    /// Reports whether this token carries recovered occurrence authority.
39    #[must_use]
40    pub const fn is_recovered(&self) -> bool {
41        self.recovered.is_some()
42    }
43
44    /// Returns the closed durable intent shape without surrendering authority.
45    #[must_use]
46    pub const fn intent(&self) -> Option<SealedBindingFateIntent> {
47        match (&self.ordinary, &self.recovered) {
48            (Some(_), None) => Some(SealedBindingFateIntent::Ordinary),
49            (None, Some(proof)) => Some(SealedBindingFateIntent::Recovered {
50                prior_binding_epoch: proof.prior_binding_epoch(),
51                marker_delivery_seq: proof.marker_delivery_seq(),
52            }),
53            (None, None) | (Some(_), Some(_)) => None,
54        }
55    }
56
57    #[cfg(test)]
58    pub(in crate::lifecycle) const fn from_recovered_for_test(
59        recovered: FencedAttachCommit,
60    ) -> Self {
61        let cursor = recovered.marker_delivery_seq();
62        Self {
63            ordinary: None,
64            recovered: Some(recovered),
65            cursor,
66        }
67    }
68
69    /// Replays one protocol-selected normal acknowledgement into this token.
70    pub(in crate::lifecycle) fn participant_ack_progressed(
71        mut self,
72        conversation_id: ConversationId,
73        participant_id: ParticipantId,
74        binding_epoch: BindingEpoch,
75        previous_cursor: DeliverySeq,
76        through_seq: DeliverySeq,
77    ) -> Result<Self, Box<Self>> {
78        if previous_cursor != self.cursor || through_seq <= previous_cursor {
79            return Err(Box::new(self));
80        }
81        match (self.ordinary.take(), self.recovered.as_ref()) {
82            (Some(authority), None) => match authority.participant_ack_progressed(
83                conversation_id,
84                participant_id,
85                binding_epoch,
86                previous_cursor,
87                through_seq,
88            ) {
89                Ok(authority) => self.ordinary = Some(authority),
90                Err(authority) => {
91                    self.ordinary = Some(authority);
92                    return Err(Box::new(self));
93                }
94            },
95            (None, Some(proof))
96                if proof.conversation_id() == conversation_id
97                    && proof.participant_id() == participant_id
98                    && proof.new_binding_epoch() == binding_epoch => {}
99            (ordinary, _) => {
100                self.ordinary = ordinary;
101                return Err(Box::new(self));
102            }
103        }
104        self.cursor = through_seq;
105        Ok(self)
106    }
107
108    /// Replays one protocol-selected MARKER acknowledgement into this token.
109    ///
110    /// A marker-ack advances the member's cursor exactly as an ordinary ack
111    /// does, so the sealed token must follow it or the two disagree forever.
112    /// The validation is therefore delegated, unchanged, to
113    /// [`Self::participant_ack_progressed`]: the token tracks CURSOR
114    /// PROGRESSION, and the operation that caused the progression does not
115    /// change what has to hold.
116    ///
117    /// # ⛔ THE IDEMPOTENT ARM IS LOAD-BEARING AND ITS ABSENCE IS INVISIBLE BY MESSAGE
118    ///
119    /// `MarkerAckCommit::apply_to` is idempotent by documented contract:
120    /// re-applying it to its own resulting cursor is a no-op returning the same
121    /// outcome. The token is progressed alongside it, so the token must tolerate
122    /// that same second application. Without the arm below, replaying an
123    /// already-applied marker-ack fails `previous_cursor != self.cursor` and
124    /// raises `ack cursor commit disagrees with sealed binding-fate authority`
125    /// — **THE EXACT STRING THE MISSING-PROGRESSION DEFECT RAISES.**
126    ///
127    /// A fix whose failure mode is message-identical to the bug it repairs
128    /// cannot be distinguished from that bug by any test asserting on the
129    /// message: such a test passes in both worlds. That is why the units
130    /// guarding this discriminate on STATE — whether the next ordinary ack
131    /// COMMITS — and never on the refusal text.
132    pub(in crate::lifecycle) fn marker_ack_progressed(
133        self,
134        conversation_id: ConversationId,
135        participant_id: ParticipantId,
136        binding_epoch: BindingEpoch,
137        previous_cursor: DeliverySeq,
138        through_seq: DeliverySeq,
139    ) -> Result<Self, Box<Self>> {
140        // ALREADY APPLIED: the token already sits at this commit's resulting
141        // cursor, so the durable row is being replayed onto state that has
142        // consumed it. `previous_cursor < through_seq` keeps a degenerate
143        // no-progress commit out of this arm — that case falls through and is
144        // refused below exactly as it always was.
145        if self.cursor == through_seq && previous_cursor < through_seq {
146            return Ok(self);
147        }
148        self.participant_ack_progressed(
149            conversation_id,
150            participant_id,
151            binding_epoch,
152            previous_cursor,
153            through_seq,
154        )
155    }
156
157    /// Returns the exact protocol-owned identity whose floor must be measured.
158    pub(in crate::lifecycle) const fn measurement_context(
159        &self,
160    ) -> Option<BindingFateMeasurementContext> {
161        match (&self.ordinary, &self.recovered) {
162            (Some(authority), None) if authority.through_seq() == self.cursor => {
163                let binding = authority.binding();
164                Some(BindingFateMeasurementContext {
165                    conversation_id: binding.conversation_id,
166                    participant_id: binding.participant_id,
167                    binding_epoch: binding.binding_epoch,
168                    cursor: self.cursor,
169                })
170            }
171            (None, Some(proof)) => Some(BindingFateMeasurementContext {
172                conversation_id: proof.conversation_id(),
173                participant_id: proof.participant_id(),
174                binding_epoch: proof.new_binding_epoch(),
175                cursor: self.cursor,
176            }),
177            (None | Some(_), None) | (Some(_), Some(_)) => None,
178        }
179    }
180
181    /// Consumes ordinary authority and the exact committed Died terminal.
182    pub(in crate::lifecycle) fn ordinary_binding_fate(
183        mut self,
184        terminal: CommittedDiedTerminal,
185        resulting_floor: DeliverySeq,
186    ) -> Result<OrdinaryBindingFate, Box<Self>> {
187        if self.recovered.is_some() {
188            return Err(Box::new(self));
189        }
190        let Some(authority) = self.ordinary.take() else {
191            return Err(Box::new(self));
192        };
193        match authority.binding_fate(terminal, resulting_floor) {
194            Ok(fate) => Ok(fate),
195            Err(authority) => {
196                self.ordinary = Some(authority);
197                Err(Box::new(self))
198            }
199        }
200    }
201
202    /// Consumes recovered authority using a protocol-measured floor.
203    pub(in crate::lifecycle) fn recovered_binding_fate_measured(
204        self,
205        resulting_floor: DeliverySeq,
206    ) -> Result<RecoveredBindingFate, Box<Self>> {
207        let Some(context) = self.measurement_context() else {
208            return Err(Box::new(self));
209        };
210        self.recovered_binding_fate(Event::binding_fate_observed(
211            context.participant_id,
212            context.binding_epoch,
213            resulting_floor,
214        ))
215    }
216
217    pub(in crate::lifecycle) fn recovered_binding_fate(
218        mut self,
219        event: Event,
220    ) -> Result<RecoveredBindingFate, Box<Self>> {
221        if self.ordinary.is_some() {
222            return Err(Box::new(self));
223        }
224        let Some(proof) = self.recovered.take() else {
225            return Err(Box::new(self));
226        };
227        match proof.recovered_binding_fate(event) {
228            Ok(fate) => Ok(fate),
229            Err(proof) => {
230                self.recovered = Some(*proof);
231                Err(Box::new(self))
232            }
233        }
234    }
235}