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 #[must_use]
307 #[allow(
308 dead_code,
309 reason = "the crate-owned event replay boundary persists this producer-emitted origin"
310 )]
311 pub(crate) const fn binding_origin(&self) -> BindingOrigin {
312 self.binding_origin
313 }
314}
315
316#[derive(Clone, Copy, Debug, PartialEq, Eq)]
318pub enum EnrollmentCommitError {
319 Conversation,
321 BindingGeneration,
323 ReceiptInvariant,
325}
326
327pub fn commit_enrollment<F, P>(
334 request: &EnrollmentRequest,
335 parameters: EnrollmentCommitParameters<F, P>,
336) -> Result<EnrollmentCommit<F>, EnrollmentCommitError> {
337 if request.conversation_id != parameters.allocated_slot.conversation_id {
338 return Err(EnrollmentCommitError::Conversation);
339 }
340 if parameters.origin_binding_epoch.capability_generation != crate::wire::Generation::ONE {
341 return Err(EnrollmentCommitError::BindingGeneration);
342 }
343 let participant_id = parameters.allocated_slot.participant_id;
344 let binding = ActiveBinding {
345 participant_id,
346 conversation_id: request.conversation_id,
347 binding_epoch: parameters.origin_binding_epoch,
348 };
349 let attached = AttachedLifecycleRecord::from_binding(binding, parameters.attached_position);
350 let Some(outcome) = EnrollBound::new(
351 request.conversation_id,
352 request.enrollment_token,
353 participant_id,
354 parameters.attach_secret,
355 parameters.origin_binding_epoch,
356 parameters.receipt_expires_at,
357 parameters.provenance_expires_at,
358 ) else {
359 return Err(EnrollmentCommitError::ReceiptInvariant);
360 };
361 let member = LiveMember::from_enrollment(
362 participant_id,
363 request.conversation_id,
364 parameters.attach_secret,
365 parameters.enrollment_fingerprint,
366 );
367 let _consumed_allocator_proof = parameters.allocated_slot.allocator_proof;
368 Ok(EnrollmentCommit {
369 member,
370 binding_state: BindingState::Bound(binding),
371 attached,
372 outcome,
373 binding_origin: BindingOrigin::unfenced(attached),
374 })
375}