1use alloc::boxed::Box;
2
3use crate::wire::{
4 AttachBound, AttachSecret, BindingEpoch, CredentialAttachRequest, DeliverySeq, Generation,
5};
6
7use super::{
8 ActiveBinding, AttachedLifecycleRecord, AttachedRecordPosition, BindingOrigin, BindingState,
9 ClosureState, CommittedBindingTerminal, CommittedBindingTerminalPosition,
10 CommittedDetachedTerminal, CommittedDiedTerminal, DetachCell, Event, FencedAttachCommit,
11 LiveMember, MembershipInvariantError, ObserverProgressProjection, OrdinaryBindingAuthority,
12 OrdinaryBindingFate, OrdinaryDetachedAttachAdmission, PendingFinalization,
13 detach::validate_pending_pair, lookup::AttachSecretProof,
14};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct AttachCommitParameters {
19 pub binding: ActiveBinding,
21 pub attach_secret: AttachSecret,
23 pub attached_position: AttachedRecordPosition,
25 pub receipt_expires_at: u128,
27 pub provenance_expires_at: u128,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum AttachVerificationError {
34 Conversation,
36 Participant,
38 Generation,
40 Secret,
42 BindingState,
44 MarkerProof,
46 RecoveryAuthority,
48 PendingTerminalSequence,
50 ResultBinding,
52 ResultGeneration,
54 LifecycleOrder,
56 TerminalHistory,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum AttachCommitError {
63 PendingDetach,
65 DetachCellAuthority,
67 BindingCellState,
69 TerminalHistory,
71 MembershipInvariant(MembershipInvariantError),
73 ReceiptInvariant,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum AttachTransition {
80 Detached,
82 Superseded {
84 terminal: CommittedDetachedTerminal,
86 },
87 FencedRecovery {
89 prior_binding_epoch: BindingEpoch,
91 composed_terminal: Option<CommittedBindingTerminal>,
93 next_closure_state: ClosureState,
95 },
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct AttachCommit<F, V> {
101 pub member: LiveMember<F>,
103 pub binding_state: BindingState,
105 pub detach_cell: DetachCell<V>,
107 pub attached: AttachedLifecycleRecord,
109 pub outcome: AttachBound,
111 pub transition: AttachTransition,
113 binding_origin: BindingOrigin,
114 ordinary_binding_authority: Option<OrdinaryBindingAuthority>,
115}
116
117impl<F, V> AttachCommit<F, V> {
118 #[must_use]
120 pub fn observer_progress_projection(&self) -> Option<ObserverProgressProjection> {
121 let terminal = match self.transition {
122 AttachTransition::Detached
123 | AttachTransition::FencedRecovery {
124 composed_terminal: None,
125 ..
126 } => return None,
127 AttachTransition::Superseded { terminal } => terminal.into(),
128 AttachTransition::FencedRecovery {
129 composed_terminal: Some(terminal),
130 ..
131 } => terminal,
132 };
133 Some(ObserverProgressProjection::new(
134 terminal.conversation_id(),
135 terminal.delivery_seq(),
136 ))
137 }
138
139 pub fn ordinary_cursor_progressed(mut self, event: Event) -> Result<Self, Box<Self>> {
149 let Some(authority) = self.ordinary_binding_authority.take() else {
150 return Err(Box::new(self));
151 };
152 match authority.cursor_progressed(event) {
153 Ok(authority) => {
154 self.ordinary_binding_authority = Some(authority);
155 Ok(self)
156 }
157 Err(authority) => {
158 self.ordinary_binding_authority = Some(authority);
159 Err(Box::new(self))
160 }
161 }
162 }
163
164 pub fn ordinary_binding_fate(
175 mut self,
176 terminal: CommittedDiedTerminal,
177 resulting_floor: DeliverySeq,
178 ) -> Result<OrdinaryBindingFate, Box<Self>> {
179 let Some(authority) = self.ordinary_binding_authority.take() else {
180 return Err(Box::new(self));
181 };
182 match authority.binding_fate(terminal, resulting_floor) {
183 Ok(fate) => Ok(fate),
184 Err(authority) => {
185 self.ordinary_binding_authority = Some(authority);
186 Err(Box::new(self))
187 }
188 }
189 }
190
191 #[must_use]
196 #[allow(
197 dead_code,
198 reason = "the crate-owned binding-fate operation consumes this sealed attach authority"
199 )]
200 pub(crate) const fn ordinary_binding_authority(&self) -> Option<OrdinaryBindingAuthority> {
201 self.ordinary_binding_authority
202 }
203
204 #[must_use]
206 #[allow(
207 dead_code,
208 reason = "the crate-owned event replay boundary persists this producer-emitted origin"
209 )]
210 pub(crate) const fn binding_origin(&self) -> BindingOrigin {
211 self.binding_origin
212 }
213}
214
215#[derive(Clone, Copy, Debug)]
216enum AttachMode<'a> {
217 Detached(OrdinaryDetachedAttachAdmission),
218 Superseded(CommittedDetachedTerminal),
219 Fenced {
220 proof: &'a FencedAttachCommit,
221 pending: Option<(PendingFinalization, DeliverySeq)>,
222 },
223}
224
225#[derive(Clone, Debug)]
227pub struct VerifiedAttachCommit<'a, F> {
228 member: LiveMember<F>,
229 request: CredentialAttachRequest,
230 parameters: AttachCommitParameters,
231 mode: AttachMode<'a>,
232}
233
234impl<F> LiveMember<F> {
235 pub fn verify_detached_attach(
241 self,
242 binding_state: BindingState,
243 closure_admission: OrdinaryDetachedAttachAdmission,
244 request: CredentialAttachRequest,
245 secret_proof: AttachSecretProof,
246 parameters: AttachCommitParameters,
247 ) -> Result<VerifiedAttachCommit<'static, F>, AttachVerificationError> {
248 self.verify_attach_common(&request, secret_proof, ¶meters)?;
249 if binding_state != BindingState::Detached {
250 return Err(AttachVerificationError::BindingState);
251 }
252 if request.accept_marker_delivery_seq.is_some() {
253 return Err(AttachVerificationError::MarkerProof);
254 }
255 Ok(VerifiedAttachCommit {
256 member: self,
257 request,
258 parameters,
259 mode: AttachMode::Detached(closure_admission),
260 })
261 }
262
263 pub fn verify_superseding_attach(
270 self,
271 active_binding: ActiveBinding,
272 request: CredentialAttachRequest,
273 secret_proof: AttachSecretProof,
274 terminal_position: CommittedBindingTerminalPosition,
275 parameters: AttachCommitParameters,
276 ) -> Result<VerifiedAttachCommit<'static, F>, AttachVerificationError> {
277 self.verify_attach_common(&request, secret_proof, ¶meters)?;
278 if active_binding.conversation_id != self.conversation_id()
279 || active_binding.participant_id != self.participant_id()
280 || active_binding.binding_epoch.capability_generation != self.generation()
281 {
282 return Err(AttachVerificationError::BindingState);
283 }
284 if request.accept_marker_delivery_seq.is_some() {
285 return Err(AttachVerificationError::MarkerProof);
286 }
287 if terminal_position.transaction_order() != parameters.attached_position.transaction_order()
288 {
289 return Err(AttachVerificationError::LifecycleOrder);
290 }
291 let terminal = active_binding.superseded(terminal_position);
292 Ok(VerifiedAttachCommit {
293 member: self,
294 request,
295 parameters,
296 mode: AttachMode::Superseded(terminal),
297 })
298 }
299
300 pub fn verify_fenced_attach(
307 self,
308 binding_state: BindingState,
309 request: CredentialAttachRequest,
310 secret_proof: AttachSecretProof,
311 proof: &FencedAttachCommit,
312 pending_terminal_delivery_seq: Option<DeliverySeq>,
313 parameters: AttachCommitParameters,
314 ) -> Result<VerifiedAttachCommit<'_, F>, AttachVerificationError> {
315 self.verify_attach_common(&request, secret_proof, ¶meters)?;
316 if request.accept_marker_delivery_seq != Some(proof.marker_delivery_seq()) {
317 return Err(AttachVerificationError::MarkerProof);
318 }
319 if proof.participant_id() != self.participant_id()
320 || proof.prior_binding_epoch().capability_generation != self.generation()
321 || proof.new_binding_epoch() != parameters.binding.binding_epoch
322 {
323 return Err(AttachVerificationError::RecoveryAuthority);
324 }
325 let pending = match (binding_state, pending_terminal_delivery_seq) {
326 (BindingState::Detached, None) => {
327 if self
328 .latest_terminal()
329 .is_none_or(|terminal| terminal.binding_epoch() != proof.prior_binding_epoch())
330 {
331 return Err(AttachVerificationError::TerminalHistory);
332 }
333 None
334 }
335 (BindingState::PendingFinalization(finalization), Some(sequence)) => {
336 let same_conversation = finalization.conversation_id() == self.conversation_id();
337 let same_participant = finalization.participant_id() == self.participant_id();
338 let same_prior_epoch = finalization.binding_epoch() == proof.prior_binding_epoch();
339 if !(same_conversation && same_participant && same_prior_epoch) {
340 return Err(AttachVerificationError::BindingState);
341 }
342 Some((finalization, sequence))
343 }
344 (BindingState::PendingFinalization(_), None) | (BindingState::Detached, Some(_)) => {
345 return Err(AttachVerificationError::PendingTerminalSequence);
346 }
347 (BindingState::Bound(_), _) => {
348 return Err(AttachVerificationError::BindingState);
349 }
350 };
351 Ok(VerifiedAttachCommit {
352 member: self,
353 request,
354 parameters,
355 mode: AttachMode::Fenced { proof, pending },
356 })
357 }
358
359 fn verify_attach_common(
360 &self,
361 request: &CredentialAttachRequest,
362 secret_proof: AttachSecretProof,
363 parameters: &AttachCommitParameters,
364 ) -> Result<(), AttachVerificationError> {
365 if request.conversation_id != self.conversation_id() {
366 return Err(AttachVerificationError::Conversation);
367 }
368 if request.participant_id != self.participant_id() {
369 return Err(AttachVerificationError::Participant);
370 }
371 if request.capability_generation != self.generation() {
372 return Err(AttachVerificationError::Generation);
373 }
374 if secret_proof == AttachSecretProof::Mismatch {
375 return Err(AttachVerificationError::Secret);
376 }
377 if parameters.binding.conversation_id != self.conversation_id()
378 || parameters.binding.participant_id != self.participant_id()
379 {
380 return Err(AttachVerificationError::ResultBinding);
381 }
382 let Some(next_raw) = self.generation().get().checked_add(1) else {
383 return Err(AttachVerificationError::ResultGeneration);
384 };
385 let Some(next_generation) = Generation::new(next_raw) else {
386 return Err(AttachVerificationError::ResultGeneration);
387 };
388 if parameters.binding.binding_epoch.capability_generation != next_generation {
389 return Err(AttachVerificationError::ResultGeneration);
390 }
391 Ok(())
392 }
393}
394
395pub fn commit_attach<F, V>(
402 verified: VerifiedAttachCommit<'_, F>,
403 detach_cell: DetachCell<V>,
404) -> Result<AttachCommit<F, V>, AttachCommitError>
405where
406 V: Copy + Eq,
407{
408 let VerifiedAttachCommit {
409 member: previous_member,
410 request,
411 parameters,
412 mode,
413 } = verified;
414 let next_cell = transition_detach_cell(&mode, &previous_member, detach_cell)?;
415 let attached =
416 AttachedLifecycleRecord::from_binding(parameters.binding, parameters.attached_position);
417 let (persisted_cursor, terminal, transition, binding_origin) = match mode {
418 AttachMode::Detached(_) => (
419 previous_member.cursor(),
420 None,
421 AttachTransition::Detached,
422 BindingOrigin::unfenced(attached),
423 ),
424 AttachMode::Superseded(committed) => (
425 previous_member.cursor(),
426 Some(CommittedBindingTerminal::from(committed)),
427 AttachTransition::Superseded {
428 terminal: committed,
429 },
430 BindingOrigin::unfenced(attached),
431 ),
432 AttachMode::Fenced { proof, pending } => {
433 let composed_terminal =
434 pending.map(|(finalization, sequence)| finalization.commit(sequence));
435 (
436 proof.marker_delivery_seq(),
437 composed_terminal,
438 AttachTransition::FencedRecovery {
439 prior_binding_epoch: proof.prior_binding_epoch(),
440 composed_terminal,
441 next_closure_state: proof.next_state(),
442 },
443 BindingOrigin::recovered(
444 attached,
445 proof.marker_delivery_seq(),
446 proof.prior_binding_epoch(),
447 ),
448 )
449 }
450 };
451 let result_generation = parameters.binding.binding_epoch.capability_generation;
452 let member = previous_member
453 .rotate(
454 result_generation,
455 parameters.attach_secret,
456 persisted_cursor,
457 terminal,
458 )
459 .map_err(AttachCommitError::MembershipInvariant)?;
460 let outcome = match transition {
461 AttachTransition::FencedRecovery { .. } => AttachBound::fenced(
462 request.conversation_id,
463 request.attach_attempt_token,
464 request.participant_id,
465 request.capability_generation,
466 parameters.attach_secret,
467 parameters.binding.binding_epoch,
468 persisted_cursor,
469 parameters.receipt_expires_at,
470 parameters.provenance_expires_at,
471 ),
472 AttachTransition::Detached | AttachTransition::Superseded { .. } => AttachBound::ordinary(
473 request.conversation_id,
474 request.attach_attempt_token,
475 request.participant_id,
476 request.capability_generation,
477 parameters.attach_secret,
478 parameters.binding.binding_epoch,
479 persisted_cursor,
480 parameters.receipt_expires_at,
481 parameters.provenance_expires_at,
482 ),
483 }
484 .ok_or(AttachCommitError::ReceiptInvariant)?;
485 let ordinary_binding_authority = match transition {
486 AttachTransition::Detached | AttachTransition::Superseded { .. } => Some(
487 OrdinaryBindingAuthority::new(parameters.binding, persisted_cursor),
488 ),
489 AttachTransition::FencedRecovery { .. } => None,
490 };
491 Ok(AttachCommit {
492 member,
493 binding_state: BindingState::Bound(parameters.binding),
494 detach_cell: next_cell,
495 attached,
496 outcome,
497 transition,
498 binding_origin,
499 ordinary_binding_authority,
500 })
501}
502
503fn transition_detach_cell<F, V>(
504 mode: &AttachMode<'_>,
505 member: &LiveMember<F>,
506 detach_cell: DetachCell<V>,
507) -> Result<DetachCell<V>, AttachCommitError>
508where
509 V: Copy + Eq,
510{
511 match detach_cell {
512 DetachCell::Empty(cell) => Ok(DetachCell::Empty(cell)),
513 DetachCell::Pending(cell) => {
514 let AttachMode::Fenced {
515 pending: Some((binding_state, _)),
516 ..
517 } = mode
518 else {
519 return Err(AttachCommitError::PendingDetach);
520 };
521 validate_pending_pair(
522 BindingState::PendingFinalization(*binding_state),
523 &cell,
524 Some(member.conversation_id()),
525 )
526 .map_err(|_| AttachCommitError::BindingCellState)?;
527 Ok(DetachCell::Terminalized(cell.terminalize_after_attach()))
528 }
529 DetachCell::Committed(cell) => {
530 if matches!(mode, AttachMode::Superseded(_))
531 || cell.participant_id() != member.participant_id()
532 || cell.request_generation() != member.generation()
533 {
534 return Err(AttachCommitError::DetachCellAuthority);
535 }
536 let Some(terminal) = member.latest_terminal() else {
537 return Err(AttachCommitError::TerminalHistory);
538 };
539 if terminal.detached_cause() != Some(crate::wire::DetachedCause::CleanDeregister)
540 || terminal.binding_epoch() != cell.committed_binding_epoch()
541 || terminal.delivery_seq() != cell.detached_delivery_seq()
542 {
543 return Err(AttachCommitError::TerminalHistory);
544 }
545 Ok(DetachCell::Terminalized(cell.terminalize_after_attach()))
546 }
547 DetachCell::Terminalized(cell) => {
548 if cell.participant_id() != member.participant_id() {
549 return Err(AttachCommitError::DetachCellAuthority);
550 }
551 Ok(DetachCell::Terminalized(cell))
552 }
553 }
554}