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(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 recovered_binding_authority: Option<FencedAttachCommit>,
116}
117
118#[derive(Debug, PartialEq, Eq)]
120pub struct InstalledAttachState<F, V> {
121 pub member: LiveMember<F>,
123 pub binding_state: BindingState,
125 pub detach_cell: DetachCell<V>,
127 pub attached: AttachedLifecycleRecord,
129 pub outcome: AttachBound,
131 pub transition: AttachTransition,
133 binding_origin: BindingOrigin,
134}
135
136#[derive(Debug, PartialEq, Eq)]
138pub struct SealedBindingFateToken {
139 pub(in crate::lifecycle) ordinary: Option<OrdinaryBindingAuthority>,
140 pub(in crate::lifecycle) recovered: Option<FencedAttachCommit>,
141 pub(in crate::lifecycle) cursor: DeliverySeq,
142}
143
144impl<F, V> AttachCommit<F, V> {
145 #[must_use]
147 pub fn into_slot_and_fate(self) -> (InstalledAttachState<F, V>, SealedBindingFateToken) {
148 let Self {
149 member,
150 binding_state,
151 detach_cell,
152 attached,
153 outcome,
154 transition,
155 binding_origin,
156 ordinary_binding_authority,
157 recovered_binding_authority,
158 } = self;
159 let cursor = member.cursor();
160 (
161 InstalledAttachState {
162 member,
163 binding_state,
164 detach_cell,
165 attached,
166 outcome,
167 transition,
168 binding_origin,
169 },
170 SealedBindingFateToken {
171 ordinary: ordinary_binding_authority,
172 recovered: recovered_binding_authority,
173 cursor,
174 },
175 )
176 }
177
178 #[must_use]
180 pub fn observer_progress_projection(&self) -> Option<ObserverProgressProjection> {
181 let terminal = match self.transition {
182 AttachTransition::Detached
183 | AttachTransition::FencedRecovery {
184 composed_terminal: None,
185 ..
186 } => return None,
187 AttachTransition::Superseded { terminal } => terminal.into(),
188 AttachTransition::FencedRecovery {
189 composed_terminal: Some(terminal),
190 ..
191 } => terminal,
192 };
193 Some(ObserverProgressProjection::new(
194 terminal.conversation_id(),
195 terminal.delivery_seq(),
196 ))
197 }
198
199 pub fn ordinary_cursor_progressed(mut self, event: Event) -> Result<Self, Box<Self>> {
209 let Some(authority) = self.ordinary_binding_authority.take() else {
210 return Err(Box::new(self));
211 };
212 match authority.cursor_progressed(event) {
213 Ok(authority) => {
214 self.ordinary_binding_authority = Some(authority);
215 Ok(self)
216 }
217 Err(authority) => {
218 self.ordinary_binding_authority = Some(authority);
219 Err(Box::new(self))
220 }
221 }
222 }
223
224 pub fn ordinary_binding_fate(
235 mut self,
236 terminal: CommittedDiedTerminal,
237 resulting_floor: DeliverySeq,
238 ) -> Result<OrdinaryBindingFate, Box<Self>> {
239 let Some(authority) = self.ordinary_binding_authority.take() else {
240 return Err(Box::new(self));
241 };
242 match authority.binding_fate(terminal, resulting_floor) {
243 Ok(fate) => Ok(fate),
244 Err(authority) => {
245 self.ordinary_binding_authority = Some(authority);
246 Err(Box::new(self))
247 }
248 }
249 }
250
251 #[must_use]
256 #[allow(
257 dead_code,
258 reason = "the crate-owned binding-fate operation consumes this sealed attach authority"
259 )]
260 pub(crate) const fn ordinary_binding_authority(&self) -> Option<OrdinaryBindingAuthority> {
261 self.ordinary_binding_authority
262 }
263
264 #[must_use]
266 #[allow(
267 dead_code,
268 reason = "the crate-owned event replay boundary persists this producer-emitted origin"
269 )]
270 pub(crate) const fn binding_origin(&self) -> BindingOrigin {
271 self.binding_origin
272 }
273}
274
275#[derive(Debug)]
276enum AttachMode {
277 Detached(OrdinaryDetachedAttachAdmission),
278 Superseded(CommittedDetachedTerminal),
279 Fenced {
280 proof: FencedAttachCommit,
281 pending: Option<(PendingFinalization, DeliverySeq)>,
282 },
283}
284
285#[derive(Debug)]
287pub struct VerifiedAttachCommit<F> {
288 member: LiveMember<F>,
289 request: CredentialAttachRequest,
290 parameters: AttachCommitParameters,
291 mode: AttachMode,
292}
293
294#[derive(Debug)]
296pub struct FencedAttachVerificationRefusal<F> {
297 member: LiveMember<F>,
298 proof: FencedAttachCommit,
299 error: AttachVerificationError,
300}
301
302impl<F> FencedAttachVerificationRefusal<F> {
303 #[must_use]
305 pub const fn error(&self) -> AttachVerificationError {
306 self.error
307 }
308
309 #[must_use]
311 pub fn into_parts(self) -> (LiveMember<F>, FencedAttachCommit) {
312 (self.member, self.proof)
313 }
314}
315
316impl<F> LiveMember<F> {
317 pub fn verify_detached_attach(
323 self,
324 binding_state: BindingState,
325 closure_admission: OrdinaryDetachedAttachAdmission,
326 request: CredentialAttachRequest,
327 secret_proof: AttachSecretProof,
328 parameters: AttachCommitParameters,
329 ) -> Result<VerifiedAttachCommit<F>, AttachVerificationError> {
330 self.verify_attach_common(&request, secret_proof, ¶meters)?;
331 if binding_state != BindingState::Detached {
332 return Err(AttachVerificationError::BindingState);
333 }
334 if request.accept_marker_delivery_seq.is_some() {
335 return Err(AttachVerificationError::MarkerProof);
336 }
337 Ok(VerifiedAttachCommit {
338 member: self,
339 request,
340 parameters,
341 mode: AttachMode::Detached(closure_admission),
342 })
343 }
344
345 pub fn verify_superseding_attach(
352 self,
353 active_binding: ActiveBinding,
354 request: CredentialAttachRequest,
355 secret_proof: AttachSecretProof,
356 terminal_position: CommittedBindingTerminalPosition,
357 parameters: AttachCommitParameters,
358 ) -> Result<VerifiedAttachCommit<F>, AttachVerificationError> {
359 self.verify_attach_common(&request, secret_proof, ¶meters)?;
360 if active_binding.conversation_id != self.conversation_id()
361 || active_binding.participant_id != self.participant_id()
362 || active_binding.binding_epoch.capability_generation != self.generation()
363 {
364 return Err(AttachVerificationError::BindingState);
365 }
366 if request.accept_marker_delivery_seq.is_some() {
367 return Err(AttachVerificationError::MarkerProof);
368 }
369 if terminal_position.transaction_order() != parameters.attached_position.transaction_order()
370 {
371 return Err(AttachVerificationError::LifecycleOrder);
372 }
373 let terminal = active_binding.superseded(terminal_position);
374 Ok(VerifiedAttachCommit {
375 member: self,
376 request,
377 parameters,
378 mode: AttachMode::Superseded(terminal),
379 })
380 }
381
382 pub fn verify_fenced_attach(
389 self,
390 binding_state: BindingState,
391 request: CredentialAttachRequest,
392 secret_proof: AttachSecretProof,
393 proof: FencedAttachCommit,
394 pending_terminal_delivery_seq: Option<DeliverySeq>,
395 parameters: AttachCommitParameters,
396 ) -> Result<VerifiedAttachCommit<F>, Box<FencedAttachVerificationRefusal<F>>> {
397 let pending = match self.validate_fenced_attach(
398 binding_state,
399 &request,
400 secret_proof,
401 &proof,
402 pending_terminal_delivery_seq,
403 ¶meters,
404 ) {
405 Ok(pending) => pending,
406 Err(error) => {
407 return Err(Box::new(FencedAttachVerificationRefusal {
408 member: self,
409 proof,
410 error,
411 }));
412 }
413 };
414 Ok(VerifiedAttachCommit {
415 member: self,
416 request,
417 parameters,
418 mode: AttachMode::Fenced { proof, pending },
419 })
420 }
421
422 fn validate_fenced_attach(
423 &self,
424 binding_state: BindingState,
425 request: &CredentialAttachRequest,
426 secret_proof: AttachSecretProof,
427 proof: &FencedAttachCommit,
428 pending_terminal_delivery_seq: Option<DeliverySeq>,
429 parameters: &AttachCommitParameters,
430 ) -> Result<Option<(PendingFinalization, DeliverySeq)>, AttachVerificationError> {
431 self.verify_attach_common(request, secret_proof, parameters)?;
432 if request.accept_marker_delivery_seq != Some(proof.marker_delivery_seq()) {
433 return Err(AttachVerificationError::MarkerProof);
434 }
435 if proof.participant_id() != self.participant_id()
436 || proof.prior_binding_epoch().capability_generation != self.generation()
437 || proof.new_binding_epoch() != parameters.binding.binding_epoch
438 {
439 return Err(AttachVerificationError::RecoveryAuthority);
440 }
441 match (binding_state, pending_terminal_delivery_seq) {
442 (BindingState::Detached, None) => {
443 if self
444 .latest_terminal()
445 .is_none_or(|terminal| terminal.binding_epoch() != proof.prior_binding_epoch())
446 {
447 return Err(AttachVerificationError::TerminalHistory);
448 }
449 Ok(None)
450 }
451 (BindingState::PendingFinalization(finalization), Some(sequence)) => {
452 let same_conversation = finalization.conversation_id() == self.conversation_id();
453 let same_participant = finalization.participant_id() == self.participant_id();
454 let same_prior_epoch = finalization.binding_epoch() == proof.prior_binding_epoch();
455 if !(same_conversation && same_participant && same_prior_epoch) {
456 return Err(AttachVerificationError::BindingState);
457 }
458 Ok(Some((finalization, sequence)))
459 }
460 (BindingState::PendingFinalization(_), None) | (BindingState::Detached, Some(_)) => {
461 Err(AttachVerificationError::PendingTerminalSequence)
462 }
463 (BindingState::Bound(_), _) => Err(AttachVerificationError::BindingState),
464 }
465 }
466
467 fn verify_attach_common(
468 &self,
469 request: &CredentialAttachRequest,
470 secret_proof: AttachSecretProof,
471 parameters: &AttachCommitParameters,
472 ) -> Result<(), AttachVerificationError> {
473 if request.conversation_id != self.conversation_id() {
474 return Err(AttachVerificationError::Conversation);
475 }
476 if request.participant_id != self.participant_id() {
477 return Err(AttachVerificationError::Participant);
478 }
479 if request.capability_generation != self.generation() {
480 return Err(AttachVerificationError::Generation);
481 }
482 if secret_proof == AttachSecretProof::Mismatch {
483 return Err(AttachVerificationError::Secret);
484 }
485 if parameters.binding.conversation_id != self.conversation_id()
486 || parameters.binding.participant_id != self.participant_id()
487 {
488 return Err(AttachVerificationError::ResultBinding);
489 }
490 let Some(next_raw) = self.generation().get().checked_add(1) else {
491 return Err(AttachVerificationError::ResultGeneration);
492 };
493 let Some(next_generation) = Generation::new(next_raw) else {
494 return Err(AttachVerificationError::ResultGeneration);
495 };
496 if parameters.binding.binding_epoch.capability_generation != next_generation {
497 return Err(AttachVerificationError::ResultGeneration);
498 }
499 Ok(())
500 }
501}
502
503pub fn commit_attach<F, V>(
510 verified: VerifiedAttachCommit<F>,
511 detach_cell: DetachCell<V>,
512) -> Result<AttachCommit<F, V>, AttachCommitError>
513where
514 V: Copy + Eq,
515{
516 let VerifiedAttachCommit {
517 member: previous_member,
518 request,
519 parameters,
520 mode,
521 } = verified;
522 let next_cell = transition_detach_cell(&mode, &previous_member, detach_cell)?;
523 let attached =
524 AttachedLifecycleRecord::from_binding(parameters.binding, parameters.attached_position);
525 let (persisted_cursor, terminal, transition, binding_origin, recovered_binding_authority) =
526 match mode {
527 AttachMode::Detached(_) => (
528 previous_member.cursor(),
529 None,
530 AttachTransition::Detached,
531 BindingOrigin::unfenced(attached),
532 None,
533 ),
534 AttachMode::Superseded(committed) => (
535 previous_member.cursor(),
536 Some(CommittedBindingTerminal::from(committed)),
537 AttachTransition::Superseded {
538 terminal: committed,
539 },
540 BindingOrigin::unfenced(attached),
541 None,
542 ),
543 AttachMode::Fenced { proof, pending } => {
544 let composed_terminal =
545 pending.map(|(finalization, sequence)| finalization.commit(sequence));
546 let marker_delivery_seq = proof.marker_delivery_seq();
547 let prior_binding_epoch = proof.prior_binding_epoch();
548 let next_closure_state = proof.next_state();
549 (
550 marker_delivery_seq,
551 composed_terminal,
552 AttachTransition::FencedRecovery {
553 prior_binding_epoch,
554 composed_terminal,
555 next_closure_state,
556 },
557 BindingOrigin::recovered(attached, marker_delivery_seq, prior_binding_epoch),
558 Some(proof),
559 )
560 }
561 };
562 let result_generation = parameters.binding.binding_epoch.capability_generation;
563 let member = previous_member
564 .rotate(
565 result_generation,
566 parameters.attach_secret,
567 persisted_cursor,
568 terminal,
569 )
570 .map_err(AttachCommitError::MembershipInvariant)?;
571 let outcome = match transition {
572 AttachTransition::FencedRecovery { .. } => AttachBound::fenced(
573 request.conversation_id,
574 request.attach_attempt_token,
575 request.participant_id,
576 request.capability_generation,
577 parameters.attach_secret,
578 parameters.binding.binding_epoch,
579 persisted_cursor,
580 parameters.receipt_expires_at,
581 parameters.provenance_expires_at,
582 ),
583 AttachTransition::Detached | AttachTransition::Superseded { .. } => AttachBound::ordinary(
584 request.conversation_id,
585 request.attach_attempt_token,
586 request.participant_id,
587 request.capability_generation,
588 parameters.attach_secret,
589 parameters.binding.binding_epoch,
590 persisted_cursor,
591 parameters.receipt_expires_at,
592 parameters.provenance_expires_at,
593 ),
594 }
595 .ok_or(AttachCommitError::ReceiptInvariant)?;
596 let ordinary_binding_authority = match transition {
597 AttachTransition::Detached | AttachTransition::Superseded { .. } => Some(
598 OrdinaryBindingAuthority::new(parameters.binding, persisted_cursor),
599 ),
600 AttachTransition::FencedRecovery { .. } => None,
601 };
602 Ok(AttachCommit {
603 member,
604 binding_state: BindingState::Bound(parameters.binding),
605 detach_cell: next_cell,
606 attached,
607 outcome,
608 transition,
609 binding_origin,
610 ordinary_binding_authority,
611 recovered_binding_authority,
612 })
613}
614
615fn transition_detach_cell<F, V>(
616 mode: &AttachMode,
617 member: &LiveMember<F>,
618 detach_cell: DetachCell<V>,
619) -> Result<DetachCell<V>, AttachCommitError>
620where
621 V: Copy + Eq,
622{
623 match detach_cell {
624 DetachCell::Empty(cell) => Ok(DetachCell::Empty(cell)),
625 DetachCell::Pending(cell) => {
626 let AttachMode::Fenced {
627 pending: Some((binding_state, _)),
628 ..
629 } = mode
630 else {
631 return Err(AttachCommitError::PendingDetach);
632 };
633 validate_pending_pair(
634 BindingState::PendingFinalization(*binding_state),
635 &cell,
636 Some(member.conversation_id()),
637 )
638 .map_err(|_| AttachCommitError::BindingCellState)?;
639 Ok(DetachCell::Terminalized(cell.terminalize_after_attach()))
640 }
641 DetachCell::Committed(cell) => {
642 if matches!(mode, AttachMode::Superseded(_))
643 || cell.participant_id() != member.participant_id()
644 || cell.request_generation() != member.generation()
645 {
646 return Err(AttachCommitError::DetachCellAuthority);
647 }
648 let Some(terminal) = member.latest_terminal() else {
649 return Err(AttachCommitError::TerminalHistory);
650 };
651 if terminal.detached_cause() != Some(crate::wire::DetachedCause::CleanDeregister)
652 || terminal.binding_epoch() != cell.committed_binding_epoch()
653 || terminal.delivery_seq() != cell.detached_delivery_seq()
654 {
655 return Err(AttachCommitError::TerminalHistory);
656 }
657 Ok(DetachCell::Terminalized(cell.terminalize_after_attach()))
658 }
659 DetachCell::Terminalized(cell) => {
660 if cell.participant_id() != member.participant_id() {
661 return Err(AttachCommitError::DetachCellAuthority);
662 }
663 Ok(DetachCell::Terminalized(cell))
664 }
665 }
666}