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
136impl<F, V> InstalledAttachState<F, V> {
137 #[doc(hidden)]
139 #[must_use]
140 pub fn encode_binding_origin_source(&self) -> alloc::vec::Vec<u8> {
141 super::checkpoint_codec::encode_binding_origin_source(self.binding_origin)
142 }
143}
144
145#[derive(Debug, PartialEq, Eq)]
147pub struct SealedBindingFateToken {
148 pub(in crate::lifecycle) ordinary: Option<OrdinaryBindingAuthority>,
149 pub(in crate::lifecycle) recovered: Option<FencedAttachCommit>,
150 pub(in crate::lifecycle) cursor: DeliverySeq,
151}
152
153impl<F, V> AttachCommit<F, V> {
154 #[must_use]
156 pub fn into_slot_and_fate(self) -> (InstalledAttachState<F, V>, SealedBindingFateToken) {
157 let Self {
158 member,
159 binding_state,
160 detach_cell,
161 attached,
162 outcome,
163 transition,
164 binding_origin,
165 ordinary_binding_authority,
166 recovered_binding_authority,
167 } = self;
168 let cursor = member.cursor();
169 (
170 InstalledAttachState {
171 member,
172 binding_state,
173 detach_cell,
174 attached,
175 outcome,
176 transition,
177 binding_origin,
178 },
179 SealedBindingFateToken {
180 ordinary: ordinary_binding_authority,
181 recovered: recovered_binding_authority,
182 cursor,
183 },
184 )
185 }
186
187 #[must_use]
189 pub fn observer_progress_projection(&self) -> Option<ObserverProgressProjection> {
190 let terminal = match self.transition {
191 AttachTransition::Detached
192 | AttachTransition::FencedRecovery {
193 composed_terminal: None,
194 ..
195 } => return None,
196 AttachTransition::Superseded { terminal } => terminal.into(),
197 AttachTransition::FencedRecovery {
198 composed_terminal: Some(terminal),
199 ..
200 } => terminal,
201 };
202 Some(ObserverProgressProjection::sourced(
203 terminal.conversation_id(),
204 terminal.delivery_seq(),
205 super::checkpoint_codec::ObserverProjectionSource::Terminal(
206 terminal.storage_description(),
207 ),
208 ))
209 }
210
211 pub fn ordinary_cursor_progressed(mut self, event: Event) -> Result<Self, Box<Self>> {
221 let Some(authority) = self.ordinary_binding_authority.take() else {
222 return Err(Box::new(self));
223 };
224 match authority.cursor_progressed(event) {
225 Ok(authority) => {
226 self.ordinary_binding_authority = Some(authority);
227 Ok(self)
228 }
229 Err(authority) => {
230 self.ordinary_binding_authority = Some(authority);
231 Err(Box::new(self))
232 }
233 }
234 }
235
236 pub fn ordinary_binding_fate(
247 mut self,
248 terminal: CommittedDiedTerminal,
249 resulting_floor: DeliverySeq,
250 ) -> Result<OrdinaryBindingFate, Box<Self>> {
251 let Some(authority) = self.ordinary_binding_authority.take() else {
252 return Err(Box::new(self));
253 };
254 match authority.binding_fate(terminal, resulting_floor) {
255 Ok(fate) => Ok(fate),
256 Err(authority) => {
257 self.ordinary_binding_authority = Some(authority);
258 Err(Box::new(self))
259 }
260 }
261 }
262
263 #[must_use]
268 #[allow(
269 dead_code,
270 reason = "the crate-owned binding-fate operation consumes this sealed attach authority"
271 )]
272 pub(crate) const fn ordinary_binding_authority(&self) -> Option<OrdinaryBindingAuthority> {
273 self.ordinary_binding_authority
274 }
275
276 #[must_use]
278 #[allow(
279 dead_code,
280 reason = "the crate-owned event replay boundary persists this producer-emitted origin"
281 )]
282 pub(crate) const fn binding_origin(&self) -> BindingOrigin {
283 self.binding_origin
284 }
285}
286
287#[derive(Debug)]
288enum AttachMode {
289 Detached(OrdinaryDetachedAttachAdmission),
290 Superseded(CommittedDetachedTerminal),
291 Fenced {
292 proof: FencedAttachCommit,
293 pending: Option<(PendingFinalization, DeliverySeq)>,
294 },
295}
296
297#[derive(Debug)]
299pub struct VerifiedAttachCommit<F> {
300 member: LiveMember<F>,
301 request: CredentialAttachRequest,
302 parameters: AttachCommitParameters,
303 mode: AttachMode,
304}
305
306#[derive(Debug)]
308pub struct FencedAttachVerificationRefusal<F> {
309 member: LiveMember<F>,
310 proof: FencedAttachCommit,
311 error: AttachVerificationError,
312}
313
314impl<F> FencedAttachVerificationRefusal<F> {
315 #[must_use]
317 pub const fn error(&self) -> AttachVerificationError {
318 self.error
319 }
320
321 #[must_use]
323 pub fn into_parts(self) -> (LiveMember<F>, FencedAttachCommit) {
324 (self.member, self.proof)
325 }
326}
327
328impl<F> LiveMember<F> {
329 pub fn verify_detached_attach(
335 self,
336 binding_state: BindingState,
337 closure_admission: OrdinaryDetachedAttachAdmission,
338 request: CredentialAttachRequest,
339 secret_proof: AttachSecretProof,
340 parameters: AttachCommitParameters,
341 ) -> Result<VerifiedAttachCommit<F>, AttachVerificationError> {
342 self.verify_attach_common(&request, secret_proof, ¶meters)?;
343 if binding_state != BindingState::Detached {
344 return Err(AttachVerificationError::BindingState);
345 }
346 if request.accept_marker_delivery_seq.is_some() {
347 return Err(AttachVerificationError::MarkerProof);
348 }
349 Ok(VerifiedAttachCommit {
350 member: self,
351 request,
352 parameters,
353 mode: AttachMode::Detached(closure_admission),
354 })
355 }
356
357 pub fn verify_superseding_attach(
364 self,
365 active_binding: ActiveBinding,
366 request: CredentialAttachRequest,
367 secret_proof: AttachSecretProof,
368 terminal_position: CommittedBindingTerminalPosition,
369 parameters: AttachCommitParameters,
370 ) -> Result<VerifiedAttachCommit<F>, AttachVerificationError> {
371 self.verify_attach_common(&request, secret_proof, ¶meters)?;
372 if active_binding.conversation_id != self.conversation_id()
373 || active_binding.participant_id != self.participant_id()
374 || active_binding.binding_epoch.capability_generation != self.generation()
375 {
376 return Err(AttachVerificationError::BindingState);
377 }
378 if request.accept_marker_delivery_seq.is_some() {
379 return Err(AttachVerificationError::MarkerProof);
380 }
381 if terminal_position.transaction_order() != parameters.attached_position.transaction_order()
382 {
383 return Err(AttachVerificationError::LifecycleOrder);
384 }
385 let terminal = active_binding.superseded(terminal_position);
386 Ok(VerifiedAttachCommit {
387 member: self,
388 request,
389 parameters,
390 mode: AttachMode::Superseded(terminal),
391 })
392 }
393
394 pub fn verify_fenced_attach(
401 self,
402 binding_state: BindingState,
403 request: CredentialAttachRequest,
404 secret_proof: AttachSecretProof,
405 proof: FencedAttachCommit,
406 pending_terminal_delivery_seq: Option<DeliverySeq>,
407 parameters: AttachCommitParameters,
408 ) -> Result<VerifiedAttachCommit<F>, Box<FencedAttachVerificationRefusal<F>>> {
409 let pending = match self.validate_fenced_attach(
410 binding_state,
411 &request,
412 secret_proof,
413 &proof,
414 pending_terminal_delivery_seq,
415 ¶meters,
416 ) {
417 Ok(pending) => pending,
418 Err(error) => {
419 return Err(Box::new(FencedAttachVerificationRefusal {
420 member: self,
421 proof,
422 error,
423 }));
424 }
425 };
426 Ok(VerifiedAttachCommit {
427 member: self,
428 request,
429 parameters,
430 mode: AttachMode::Fenced { proof, pending },
431 })
432 }
433
434 fn validate_fenced_attach(
435 &self,
436 binding_state: BindingState,
437 request: &CredentialAttachRequest,
438 secret_proof: AttachSecretProof,
439 proof: &FencedAttachCommit,
440 pending_terminal_delivery_seq: Option<DeliverySeq>,
441 parameters: &AttachCommitParameters,
442 ) -> Result<Option<(PendingFinalization, DeliverySeq)>, AttachVerificationError> {
443 self.verify_attach_common(request, secret_proof, parameters)?;
444 if request.accept_marker_delivery_seq != Some(proof.marker_delivery_seq()) {
445 return Err(AttachVerificationError::MarkerProof);
446 }
447 if proof.participant_id() != self.participant_id()
448 || proof.prior_binding_epoch().capability_generation != self.generation()
449 || proof.new_binding_epoch() != parameters.binding.binding_epoch
450 {
451 return Err(AttachVerificationError::RecoveryAuthority);
452 }
453 match (binding_state, pending_terminal_delivery_seq) {
454 (BindingState::Detached, None) => {
455 if self
456 .latest_terminal()
457 .is_none_or(|terminal| terminal.binding_epoch() != proof.prior_binding_epoch())
458 {
459 return Err(AttachVerificationError::TerminalHistory);
460 }
461 Ok(None)
462 }
463 (BindingState::PendingFinalization(finalization), Some(sequence)) => {
464 let same_conversation = finalization.conversation_id() == self.conversation_id();
465 let same_participant = finalization.participant_id() == self.participant_id();
466 let same_prior_epoch = finalization.binding_epoch() == proof.prior_binding_epoch();
467 if !(same_conversation && same_participant && same_prior_epoch) {
468 return Err(AttachVerificationError::BindingState);
469 }
470 Ok(Some((finalization, sequence)))
471 }
472 (BindingState::PendingFinalization(_), None) | (BindingState::Detached, Some(_)) => {
473 Err(AttachVerificationError::PendingTerminalSequence)
474 }
475 (BindingState::Bound(_), _) => Err(AttachVerificationError::BindingState),
476 }
477 }
478
479 fn verify_attach_common(
480 &self,
481 request: &CredentialAttachRequest,
482 secret_proof: AttachSecretProof,
483 parameters: &AttachCommitParameters,
484 ) -> Result<(), AttachVerificationError> {
485 if request.conversation_id != self.conversation_id() {
486 return Err(AttachVerificationError::Conversation);
487 }
488 if request.participant_id != self.participant_id() {
489 return Err(AttachVerificationError::Participant);
490 }
491 if request.capability_generation != self.generation() {
492 return Err(AttachVerificationError::Generation);
493 }
494 if secret_proof == AttachSecretProof::Mismatch {
495 return Err(AttachVerificationError::Secret);
496 }
497 if parameters.binding.conversation_id != self.conversation_id()
498 || parameters.binding.participant_id != self.participant_id()
499 {
500 return Err(AttachVerificationError::ResultBinding);
501 }
502 let Some(next_raw) = self.generation().get().checked_add(1) else {
503 return Err(AttachVerificationError::ResultGeneration);
504 };
505 let Some(next_generation) = Generation::new(next_raw) else {
506 return Err(AttachVerificationError::ResultGeneration);
507 };
508 if parameters.binding.binding_epoch.capability_generation != next_generation {
509 return Err(AttachVerificationError::ResultGeneration);
510 }
511 Ok(())
512 }
513}
514
515pub fn commit_attach<F, V>(
522 verified: VerifiedAttachCommit<F>,
523 detach_cell: DetachCell<V>,
524) -> Result<AttachCommit<F, V>, AttachCommitError>
525where
526 V: Copy + Eq,
527{
528 let VerifiedAttachCommit {
529 member: previous_member,
530 request,
531 parameters,
532 mode,
533 } = verified;
534 let next_cell = transition_detach_cell(&mode, &previous_member, detach_cell)?;
535 let attached =
536 AttachedLifecycleRecord::from_binding(parameters.binding, parameters.attached_position);
537 let (persisted_cursor, terminal, transition, binding_origin, recovered_binding_authority) =
538 match mode {
539 AttachMode::Detached(_) => (
540 previous_member.cursor(),
541 None,
542 AttachTransition::Detached,
543 BindingOrigin::unfenced(attached),
544 None,
545 ),
546 AttachMode::Superseded(committed) => (
547 previous_member.cursor(),
548 Some(CommittedBindingTerminal::from(committed)),
549 AttachTransition::Superseded {
550 terminal: committed,
551 },
552 BindingOrigin::unfenced(attached),
553 None,
554 ),
555 AttachMode::Fenced { proof, pending } => {
556 let composed_terminal =
557 pending.map(|(finalization, sequence)| finalization.commit(sequence));
558 let marker_delivery_seq = proof.marker_delivery_seq();
559 let prior_binding_epoch = proof.prior_binding_epoch();
560 let next_closure_state = proof.next_state();
561 let persisted_cursor = previous_member.cursor().max(marker_delivery_seq);
573 (
574 persisted_cursor,
575 composed_terminal,
576 AttachTransition::FencedRecovery {
577 prior_binding_epoch,
578 composed_terminal,
579 next_closure_state,
580 },
581 BindingOrigin::recovered(attached, marker_delivery_seq, prior_binding_epoch),
582 Some(proof),
583 )
584 }
585 };
586 let result_generation = parameters.binding.binding_epoch.capability_generation;
587 let member = previous_member
588 .rotate(
589 result_generation,
590 parameters.attach_secret,
591 persisted_cursor,
592 terminal,
593 )
594 .map_err(AttachCommitError::MembershipInvariant)?;
595 let outcome = match transition {
596 AttachTransition::FencedRecovery { .. } => AttachBound::fenced(
597 request.conversation_id,
598 request.attach_attempt_token,
599 request.participant_id,
600 request.capability_generation,
601 parameters.attach_secret,
602 parameters.binding.binding_epoch,
603 persisted_cursor,
604 parameters.receipt_expires_at,
605 parameters.provenance_expires_at,
606 ),
607 AttachTransition::Detached | AttachTransition::Superseded { .. } => AttachBound::ordinary(
608 request.conversation_id,
609 request.attach_attempt_token,
610 request.participant_id,
611 request.capability_generation,
612 parameters.attach_secret,
613 parameters.binding.binding_epoch,
614 persisted_cursor,
615 parameters.receipt_expires_at,
616 parameters.provenance_expires_at,
617 ),
618 }
619 .ok_or(AttachCommitError::ReceiptInvariant)?;
620 let ordinary_binding_authority = match transition {
621 AttachTransition::Detached | AttachTransition::Superseded { .. } => Some(
622 OrdinaryBindingAuthority::new(parameters.binding, persisted_cursor),
623 ),
624 AttachTransition::FencedRecovery { .. } => None,
625 };
626 Ok(AttachCommit {
627 member,
628 binding_state: BindingState::Bound(parameters.binding),
629 detach_cell: next_cell,
630 attached,
631 outcome,
632 transition,
633 binding_origin,
634 ordinary_binding_authority,
635 recovered_binding_authority,
636 })
637}
638
639fn transition_detach_cell<F, V>(
640 mode: &AttachMode,
641 member: &LiveMember<F>,
642 detach_cell: DetachCell<V>,
643) -> Result<DetachCell<V>, AttachCommitError>
644where
645 V: Copy + Eq,
646{
647 match detach_cell {
648 DetachCell::Empty(cell) => Ok(DetachCell::Empty(cell)),
649 DetachCell::Pending(cell) => {
650 let AttachMode::Fenced {
651 pending: Some((binding_state, _)),
652 ..
653 } = mode
654 else {
655 return Err(AttachCommitError::PendingDetach);
656 };
657 validate_pending_pair(
658 BindingState::PendingFinalization(*binding_state),
659 &cell,
660 Some(member.conversation_id()),
661 )
662 .map_err(|_| AttachCommitError::BindingCellState)?;
663 Ok(DetachCell::Terminalized(cell.terminalize_after_attach()))
664 }
665 DetachCell::Committed(cell) => {
666 if matches!(mode, AttachMode::Superseded(_))
667 || cell.participant_id() != member.participant_id()
668 || cell.request_generation() != member.generation()
669 {
670 return Err(AttachCommitError::DetachCellAuthority);
671 }
672 let Some(terminal) = member.latest_terminal() else {
673 return Err(AttachCommitError::TerminalHistory);
674 };
675 if terminal.detached_cause() != Some(crate::wire::DetachedCause::CleanDeregister)
676 || terminal.binding_epoch() != cell.committed_binding_epoch()
677 || terminal.delivery_seq() != cell.detached_delivery_seq()
678 {
679 return Err(AttachCommitError::TerminalHistory);
680 }
681 Ok(DetachCell::Terminalized(cell.terminalize_after_attach()))
682 }
683 DetachCell::Terminalized(cell) => {
684 if cell.participant_id() != member.participant_id() {
685 return Err(AttachCommitError::DetachCellAuthority);
686 }
687 Ok(DetachCell::Terminalized(cell))
688 }
689 }
690}
691
692impl SealedBindingFateToken {
693 pub fn encode_checkpoint(
699 &self,
700 ) -> Result<alloc::vec::Vec<u8>, super::OwnerCheckpointRestoreError> {
701 super::checkpoint_codec::encode_fate_token(self)
702 }
703
704 pub fn restore_checkpoint(
713 body: &[u8],
714 origin: &BindingOrigin,
715 cursor: DeliverySeq,
716 recovered: Option<FencedAttachCommit>,
717 ) -> Result<Self, super::OwnerCheckpointRestoreError> {
718 super::checkpoint_codec::restore_fate_token(body, origin, cursor, recovered)
719 }
720}