liminal_protocol/lifecycle/
enrollment.rs1use crate::outcome::CandidatePhase;
2use crate::wire::{
3 AttachSecret, BindingEpoch, ConversationId, DeliverySeq, EnrollBound, EnrollmentRequest,
4 ParticipantId, ParticipantIndex, TransactionOrder,
5};
6
7use super::{ActiveBinding, AdmissionOrder, BindingState, EnrollmentFingerprint, LiveMember};
8
9pub trait ParticipantSlotAllocatorProof {
14 fn conversation_id(&self) -> ConversationId;
16
17 fn participant_index(&self) -> ParticipantIndex;
19
20 fn identity_limit(&self) -> u64;
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct AllocatedParticipantSlot<P> {
27 allocator_proof: P,
28 conversation_id: ConversationId,
29 participant_id: ParticipantId,
30 identity_limit: u64,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum ParticipantSlotAllocationError {
36 IdentityLimit,
38}
39
40impl<P: ParticipantSlotAllocatorProof> AllocatedParticipantSlot<P> {
41 pub fn from_allocator(allocator_proof: P) -> Result<Self, ParticipantSlotAllocationError> {
48 let participant_id = allocator_proof.participant_index();
49 let identity_limit = allocator_proof.identity_limit();
50 if participant_id >= identity_limit {
51 return Err(ParticipantSlotAllocationError::IdentityLimit);
52 }
53 Ok(Self {
54 conversation_id: allocator_proof.conversation_id(),
55 participant_id,
56 identity_limit,
57 allocator_proof,
58 })
59 }
60
61 #[must_use]
63 pub const fn conversation_id(&self) -> ConversationId {
64 self.conversation_id
65 }
66
67 #[must_use]
69 pub const fn participant_id(&self) -> ParticipantId {
70 self.participant_id
71 }
72
73 #[must_use]
75 pub const fn identity_limit(&self) -> u64 {
76 self.identity_limit
77 }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub struct AttachedRecordPosition {
83 transaction_order: TransactionOrder,
84 delivery_seq: DeliverySeq,
85}
86
87impl AttachedRecordPosition {
88 #[must_use]
90 pub const fn new(transaction_order: TransactionOrder, delivery_seq: DeliverySeq) -> Self {
91 Self {
92 transaction_order,
93 delivery_seq,
94 }
95 }
96
97 #[must_use]
99 pub const fn transaction_order(self) -> TransactionOrder {
100 self.transaction_order
101 }
102
103 #[must_use]
105 pub const fn delivery_seq(self) -> DeliverySeq {
106 self.delivery_seq
107 }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub struct AttachedLifecycleRecord {
113 participant_id: ParticipantId,
114 conversation_id: ConversationId,
115 binding_epoch: BindingEpoch,
116 admission_order: AdmissionOrder,
117 delivery_seq: DeliverySeq,
118}
119
120impl AttachedLifecycleRecord {
121 pub(crate) const fn from_binding(
122 binding: ActiveBinding,
123 position: AttachedRecordPosition,
124 ) -> Self {
125 Self {
126 participant_id: binding.participant_id,
127 conversation_id: binding.conversation_id,
128 binding_epoch: binding.binding_epoch,
129 admission_order: AdmissionOrder::new(
130 position.transaction_order,
131 CandidatePhase::AttachLifecycle,
132 binding.participant_id,
133 ),
134 delivery_seq: position.delivery_seq,
135 }
136 }
137
138 #[must_use]
140 pub const fn participant_id(self) -> ParticipantId {
141 self.participant_id
142 }
143
144 #[must_use]
146 pub const fn conversation_id(self) -> ConversationId {
147 self.conversation_id
148 }
149
150 #[must_use]
152 pub const fn binding_epoch(self) -> BindingEpoch {
153 self.binding_epoch
154 }
155
156 #[must_use]
158 pub const fn admission_order(self) -> AdmissionOrder {
159 self.admission_order
160 }
161
162 #[must_use]
164 pub const fn delivery_seq(self) -> DeliverySeq {
165 self.delivery_seq
166 }
167}
168
169#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170enum BindingOriginKind {
171 Unfenced,
172 Recovered {
173 marker_delivery_seq: DeliverySeq,
174 prior_binding_epoch: BindingEpoch,
175 },
176}
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
213pub struct BindingOrigin {
214 attached: AttachedLifecycleRecord,
215 kind: BindingOriginKind,
216}
217
218impl BindingOrigin {
219 pub(crate) const fn unfenced(attached: AttachedLifecycleRecord) -> Self {
220 Self {
221 attached,
222 kind: BindingOriginKind::Unfenced,
223 }
224 }
225
226 pub(crate) const fn recovered(
227 attached: AttachedLifecycleRecord,
228 marker_delivery_seq: DeliverySeq,
229 prior_binding_epoch: BindingEpoch,
230 ) -> Self {
231 Self {
232 attached,
233 kind: BindingOriginKind::Recovered {
234 marker_delivery_seq,
235 prior_binding_epoch,
236 },
237 }
238 }
239
240 pub(crate) const fn attached(self) -> AttachedLifecycleRecord {
241 self.attached
242 }
243
244 pub(crate) const fn conversation_id(self) -> ConversationId {
245 self.attached.conversation_id()
246 }
247
248 pub(crate) const fn participant_id(self) -> ParticipantId {
249 self.attached.participant_id()
250 }
251
252 pub(crate) const fn binding_epoch(self) -> BindingEpoch {
253 self.attached.binding_epoch()
254 }
255
256 pub(crate) const fn is_unfenced(self) -> bool {
257 matches!(self.kind, BindingOriginKind::Unfenced)
258 }
259
260 pub(crate) const fn recovered_marker(self) -> Option<(DeliverySeq, BindingEpoch)> {
261 match self.kind {
262 BindingOriginKind::Unfenced => None,
263 BindingOriginKind::Recovered {
264 marker_delivery_seq,
265 prior_binding_epoch,
266 } => Some((marker_delivery_seq, prior_binding_epoch)),
267 }
268 }
269}
270
271#[derive(Clone, Debug, PartialEq, Eq)]
273pub struct EnrollmentCommitParameters<F, P> {
274 pub allocated_slot: AllocatedParticipantSlot<P>,
276 pub attach_secret: AttachSecret,
278 pub origin_binding_epoch: BindingEpoch,
280 pub attached_position: AttachedRecordPosition,
282 pub receipt_expires_at: u128,
284 pub provenance_expires_at: u128,
286 pub enrollment_fingerprint: EnrollmentFingerprint<F>,
288}
289
290#[derive(Clone, Debug, PartialEq, Eq)]
292pub struct EnrollmentCommit<F> {
293 pub member: LiveMember<F>,
295 pub binding_state: BindingState,
297 pub attached: AttachedLifecycleRecord,
299 pub outcome: EnrollBound,
301 binding_origin: BindingOrigin,
302}
303
304impl<F> EnrollmentCommit<F> {
305 #[doc(hidden)]
307 #[must_use]
308 pub fn encode_binding_origin_source(&self) -> alloc::vec::Vec<u8> {
309 super::checkpoint_codec::encode_binding_origin_source(self.binding_origin)
310 }
311
312 #[must_use]
314 #[allow(
315 dead_code,
316 reason = "the crate-owned event replay boundary persists this producer-emitted origin"
317 )]
318 pub(crate) const fn binding_origin(&self) -> BindingOrigin {
319 self.binding_origin
320 }
321}
322
323#[derive(Clone, Copy, Debug, PartialEq, Eq)]
325pub enum EnrollmentCommitError {
326 Conversation,
328 BindingGeneration,
330 ReceiptInvariant,
332}
333
334pub fn commit_enrollment<F, P>(
341 request: &EnrollmentRequest,
342 parameters: EnrollmentCommitParameters<F, P>,
343) -> Result<EnrollmentCommit<F>, EnrollmentCommitError> {
344 if request.conversation_id != parameters.allocated_slot.conversation_id {
345 return Err(EnrollmentCommitError::Conversation);
346 }
347 if parameters.origin_binding_epoch.capability_generation != crate::wire::Generation::ONE {
348 return Err(EnrollmentCommitError::BindingGeneration);
349 }
350 let participant_id = parameters.allocated_slot.participant_id;
351 let binding = ActiveBinding {
352 participant_id,
353 conversation_id: request.conversation_id,
354 binding_epoch: parameters.origin_binding_epoch,
355 };
356 let attached = AttachedLifecycleRecord::from_binding(binding, parameters.attached_position);
357 let Some(outcome) = EnrollBound::new(
358 request.conversation_id,
359 request.enrollment_token,
360 participant_id,
361 parameters.attach_secret,
362 parameters.origin_binding_epoch,
363 parameters.receipt_expires_at,
364 parameters.provenance_expires_at,
365 ) else {
366 return Err(EnrollmentCommitError::ReceiptInvariant);
367 };
368 let member = LiveMember::from_enrollment(
369 participant_id,
370 request.conversation_id,
371 parameters.attach_secret,
372 parameters.enrollment_fingerprint,
373 );
374 let _consumed_allocator_proof = parameters.allocated_slot.allocator_proof;
375 Ok(EnrollmentCommit {
376 member,
377 binding_state: BindingState::Bound(binding),
378 attached,
379 outcome,
380 binding_origin: BindingOrigin::unfenced(attached),
381 })
382}