1use alloc::collections::BTreeSet;
21use alloc::vec::Vec;
22
23use crate::wire::{ConversationId, ParticipantId};
24
25use super::checkpoint_codec::{
26 CLOSURE_SECTION_TAG, DigestDescription, FRONTIERS_SECTION_TAG, PARTICIPANTS_SECTION_TAG,
27 SEALED_SECTION_TAG, decode_description, decode_sealed_description, encode_description,
28 encode_sealed_description,
29};
30use super::claim_frontier::ClaimFrontiers;
31use super::conversation::{ConversationGenesis, ParticipantConversation};
32use super::edge::ClosureState;
33use super::membership::RetiredIdentity;
34use super::storage::{
35 ConversationDescribeError, ConversationStateRestoreError, ParticipantConversationState,
36 ParticipantLifecycleRestore, ParticipantLifecycleView, StorageRestoreError,
37 describe_conversation,
38};
39
40pub type DigestParticipantView<'a> =
42 ParticipantLifecycleView<'a, [u8; 32], [u8; 32], [u8; 32], [u8; 32]>;
43
44pub type DigestConversationState =
46 ParticipantConversationState<[u8; 32], [u8; 32], [u8; 32], [u8; 32]>;
47
48pub type DigestRetiredIdentity = RetiredIdentity<[u8; 32], [u8; 32], [u8; 32]>;
50
51type DigestParticipantRestore = ParticipantLifecycleRestore<
52 [u8; DIGEST_LEN],
53 [u8; DIGEST_LEN],
54 [u8; DIGEST_LEN],
55 [u8; DIGEST_LEN],
56>;
57
58const CAPSULE_MAGIC: [u8; 4] = *b"LPCK";
59const CAPSULE_CODEC_MAJOR: u16 = 1;
60const CAPSULE_CODEC_MINOR: u16 = 0;
63const CAPSULE_CODEC_MINOR_SEALED: u16 = 1;
66const CAPSULE_HEADER_LEN: usize = 28;
68const SECTION_HEADER_LEN: u32 = 6;
70const CONVERSATION_SECTION_TAG: u16 = 1;
71const CONVERSATION_SECTION_LEN: u32 = 9;
73const GENESIS_VALIDATED_FLAG: u8 = 0b1;
74const CONVERSATION_FRAME_LEN: u32 = SECTION_HEADER_LEN + CONVERSATION_SECTION_LEN;
76const SECTION_TAGS: [u16; 5] = [
81 CONVERSATION_SECTION_TAG,
82 PARTICIPANTS_SECTION_TAG,
83 FRONTIERS_SECTION_TAG,
84 CLOSURE_SECTION_TAG,
85 SEALED_SECTION_TAG,
86];
87const DIGEST_LEN: usize = 32;
90
91#[derive(Debug)]
94pub struct LifecycleView<'a> {
95 pub conversation: &'a ParticipantConversation,
97 pub lifecycle: Option<ConversationLifecycleView<'a>>,
100 pub sealed: Option<SealedLifecycleView<'a>>,
103 pub covered_log_sequence: u64,
105}
106
107#[derive(Debug)]
111pub struct SealedLifecycleView<'a> {
112 pub participants: Vec<DigestParticipantView<'a>>,
115 pub ledger_high_watermark: u64,
117}
118
119#[derive(Debug)]
121pub struct ConversationLifecycleView<'a> {
122 pub participants: Vec<DigestParticipantView<'a>>,
125 pub frontiers: &'a ClaimFrontiers,
127 pub retained_record_limit: u64,
129 pub closure: &'a ClosureState,
131}
132
133#[derive(Debug, PartialEq, Eq)]
137pub struct LifecycleCapsule {
138 conversation_id: ConversationId,
139 covered_log_sequence: u64,
140 conversation: ConversationSection,
141 lifecycle: Option<LifecycleSections>,
142 sealed: Option<SealedSections>,
143 body_len: u32,
146}
147
148#[derive(Debug, PartialEq, Eq)]
151struct SealedSections {
152 participants: Vec<DigestParticipantRestore>,
153 ledger_high_watermark: u64,
154 encoded: Vec<u8>,
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
159struct ConversationSection {
160 next_event_ordinal: u64,
161 genesis_validated: bool,
162}
163
164impl ConversationSection {
165 const fn is_consistent(self) -> bool {
168 self.genesis_validated == (self.next_event_ordinal > 0)
169 }
170}
171
172#[derive(Debug, PartialEq, Eq)]
175struct LifecycleSections {
176 description: DigestDescription<DIGEST_LEN>,
177 encoded: Vec<u8>,
178}
179
180impl LifecycleCapsule {
181 pub fn capture(view: &LifecycleView<'_>) -> Result<Self, CapsuleCaptureError> {
190 let conversation_id = view.conversation.conversation_id();
191 if view.lifecycle.is_some() && view.sealed.is_some() {
192 return Err(CapsuleCaptureError::SealedBesideLifecycle);
193 }
194 let lifecycle = view
195 .lifecycle
196 .as_ref()
197 .map(|lifecycle| capture_lifecycle(conversation_id, lifecycle))
198 .transpose()?;
199 let sealed = view.sealed.as_ref().map(capture_sealed).transpose()?;
200 let lifecycle_len = lifecycle
201 .as_ref()
202 .map_or(0, |sections| sections.encoded.len())
203 .checked_add(sealed.as_ref().map_or(0, |sections| sections.encoded.len()))
204 .ok_or(CapsuleCaptureError::LengthOverflow)?;
205 let body_len = u32::try_from(lifecycle_len)
206 .map_err(|_| CapsuleCaptureError::LengthOverflow)?
207 .checked_add(CONVERSATION_FRAME_LEN)
208 .ok_or(CapsuleCaptureError::LengthOverflow)?;
209 Ok(Self {
210 conversation_id,
211 covered_log_sequence: view.covered_log_sequence,
212 conversation: ConversationSection {
213 next_event_ordinal: view.conversation.next_event_ordinal(),
214 genesis_validated: view.conversation.genesis_validated(),
215 },
216 lifecycle,
217 sealed,
218 body_len,
219 })
220 }
221
222 #[must_use]
224 pub const fn is_sealed(&self) -> bool {
225 self.sealed.is_some()
226 }
227
228 #[must_use]
230 pub const fn conversation_id(&self) -> ConversationId {
231 self.conversation_id
232 }
233
234 #[must_use]
236 pub const fn covered_log_sequence(&self) -> u64 {
237 self.covered_log_sequence
238 }
239
240 #[must_use]
242 pub fn encode_canonical(&self) -> Vec<u8> {
243 let mut encoded = Vec::with_capacity(CAPSULE_HEADER_LEN + self.body_len as usize);
244 let minor = if self.sealed.is_some() {
245 CAPSULE_CODEC_MINOR_SEALED
246 } else {
247 CAPSULE_CODEC_MINOR
248 };
249 encoded.extend_from_slice(&CAPSULE_MAGIC);
250 encoded.extend_from_slice(&CAPSULE_CODEC_MAJOR.to_be_bytes());
251 encoded.extend_from_slice(&minor.to_be_bytes());
252 encoded.extend_from_slice(&self.conversation_id.to_be_bytes());
253 encoded.extend_from_slice(&self.covered_log_sequence.to_be_bytes());
254 encoded.extend_from_slice(&self.body_len.to_be_bytes());
255 encoded.extend_from_slice(&CONVERSATION_SECTION_TAG.to_be_bytes());
256 encoded.extend_from_slice(&CONVERSATION_SECTION_LEN.to_be_bytes());
257 encoded.extend_from_slice(&self.conversation.next_event_ordinal.to_be_bytes());
258 encoded.push(if self.conversation.genesis_validated {
259 GENESIS_VALIDATED_FLAG
260 } else {
261 0
262 });
263 if let Some(sections) = &self.lifecycle {
264 encoded.extend_from_slice(§ions.encoded);
265 }
266 if let Some(sections) = &self.sealed {
267 encoded.extend_from_slice(§ions.encoded);
268 }
269 encoded
270 }
271
272 pub fn decode_canonical(input: &[u8]) -> Result<Self, CapsuleDecodeError> {
282 if input.len() < CAPSULE_HEADER_LEN {
283 return Err(CapsuleDecodeError::Truncated {
284 required: CAPSULE_HEADER_LEN,
285 available: input.len(),
286 });
287 }
288 if input.get(0..4) != Some(CAPSULE_MAGIC.as_slice()) {
289 return Err(CapsuleDecodeError::InvalidMagic);
290 }
291 let major = take_u16(input, 4)?;
292 let minor = take_u16(input, 6)?;
293 if major != CAPSULE_CODEC_MAJOR || minor > CAPSULE_CODEC_MINOR_SEALED {
294 return Err(CapsuleDecodeError::UnsupportedCodec { major, minor });
295 }
296 let conversation_id = take_u64(input, 8)?;
297 let covered_log_sequence = take_u64(input, 16)?;
298 let declared_body_len = take_u32(input, 24)?;
299 let actual_body_len = input.len() - CAPSULE_HEADER_LEN;
300 if usize::try_from(declared_body_len).map_err(|_| CapsuleDecodeError::LengthOverflow)?
301 != actual_body_len
302 {
303 return Err(CapsuleDecodeError::NonCanonicalLength {
304 declared_body_len,
305 actual_body_len,
306 });
307 }
308 let mut at = CAPSULE_HEADER_LEN;
309 let conversation =
310 decode_conversation(take_section(input, &mut at, CONVERSATION_SECTION_TAG)?)?;
311 let (lifecycle, sealed) = if input.len() - at < SECTION_HEADER_LEN as usize {
312 (None, None)
313 } else {
314 let start = at;
315 let participants = take_section(input, &mut at, PARTICIPANTS_SECTION_TAG)?;
316 let next_tag = if input.len() - at < SECTION_HEADER_LEN as usize {
317 None
318 } else {
319 Some(take_u16(input, at)?)
320 };
321 if next_tag == Some(SEALED_SECTION_TAG) {
322 if minor != CAPSULE_CODEC_MINOR_SEALED {
323 return Err(CapsuleDecodeError::UnknownSection {
324 tag: SEALED_SECTION_TAG,
325 });
326 }
327 let sealed = take_section(input, &mut at, SEALED_SECTION_TAG)?;
328 let (participants, ledger_high_watermark) =
329 decode_sealed_description::<DIGEST_LEN>(participants, sealed)?;
330 let encoded = input
331 .get(start..at)
332 .ok_or(CapsuleDecodeError::LengthOverflow)?
333 .to_vec();
334 (
335 None,
336 Some(SealedSections {
337 participants,
338 ledger_high_watermark,
339 encoded,
340 }),
341 )
342 } else {
343 (
344 Some(decode_lifecycle(
345 input,
346 &mut at,
347 start,
348 participants,
349 conversation_id,
350 )?),
351 None,
352 )
353 }
354 };
355 if minor == CAPSULE_CODEC_MINOR_SEALED && sealed.is_none() {
356 return Err(CapsuleDecodeError::MissingSection {
357 tag: SEALED_SECTION_TAG,
358 });
359 }
360 if at != input.len() {
361 return Err(CapsuleDecodeError::TrailingBytes {
362 trailing: input.len() - at,
363 });
364 }
365 Ok(Self {
366 conversation_id,
367 covered_log_sequence,
368 conversation,
369 lifecycle,
370 sealed,
371 body_len: declared_body_len,
372 })
373 }
374
375 #[doc(hidden)]
382 pub fn restore_with_binding_sources(
383 self,
384 sources: &[(ParticipantId, &[u8])],
385 ) -> Result<RestoredLifecycle, BindingOriginSourceError> {
386 let mut sources = sources.iter();
387 if let Some(lifecycle) = &self.lifecycle {
388 for participant in &lifecycle.description.restore.participants {
389 if let super::storage::ParticipantLifecycleRestore::Live {
390 identity,
391 binding_origin: Some(expected),
392 ..
393 } = participant
394 {
395 let Some(&(participant_id, bytes)) = sources.next() else {
396 return Err(BindingOriginSourceError::Association {
397 participant_id: identity.participant_id,
398 });
399 };
400 let origin = super::checkpoint_codec::decode_binding_origin_source(bytes)
401 .map_err(|error| BindingOriginSourceError::Decode {
402 participant_id,
403 error,
404 })?;
405 if participant_id != identity.participant_id || origin != *expected {
406 return Err(BindingOriginSourceError::Association { participant_id });
407 }
408 }
409 }
410 }
411 if let Some(&(participant_id, _)) = sources.next() {
412 return Err(BindingOriginSourceError::Association { participant_id });
413 }
414 self.restore().map_err(BindingOriginSourceError::Restore)
415 }
416
417 pub fn restore(self) -> Result<RestoredLifecycle, CapsuleRestoreError> {
429 if !self.conversation.is_consistent() {
430 return Err(CapsuleRestoreError::InconsistentConversation);
431 }
432 let lifecycle = self
433 .lifecycle
434 .map(|sections| sections.description.into_state())
435 .transpose()
436 .map_err(CapsuleRestoreError::Lifecycle)?;
437 let sealed = self.sealed.map(restore_sealed).transpose()?;
438 Ok(RestoredLifecycle {
439 covered_log_sequence: self.covered_log_sequence,
440 restored_fenced_sources: BTreeSet::new(),
441 conversation: ParticipantConversation::from_checkpoint(
442 ConversationGenesis::new(self.conversation_id),
443 self.conversation.next_event_ordinal,
444 self.conversation.genesis_validated,
445 ),
446 lifecycle,
447 sealed,
448 })
449 }
450}
451
452fn capture_lifecycle(
455 conversation_id: ConversationId,
456 view: &ConversationLifecycleView<'_>,
457) -> Result<LifecycleSections, CapsuleCaptureError> {
458 if view.frontiers.conversation_id() != conversation_id {
459 return Err(CapsuleCaptureError::ForeignConversation);
460 }
461 for pair in view.participants.windows(2) {
462 if let [earlier, later] = pair
463 && earlier.participant_id() >= later.participant_id()
464 {
465 return Err(CapsuleCaptureError::ParticipantOutOfOrder {
466 participant_id: later.participant_id(),
467 });
468 }
469 }
470 let description = describe_conversation(
471 &view.participants,
472 view.frontiers,
473 view.retained_record_limit,
474 *view.closure,
475 )
476 .map_err(CapsuleCaptureError::Describe)?;
477 let encoded = encode_description(&description).ok_or(CapsuleCaptureError::LengthOverflow)?;
478 Ok(LifecycleSections {
479 description,
480 encoded,
481 })
482}
483
484fn capture_sealed(view: &SealedLifecycleView<'_>) -> Result<SealedSections, CapsuleCaptureError> {
487 for pair in view.participants.windows(2) {
488 if let [earlier, later] = pair
489 && earlier.participant_id() >= later.participant_id()
490 {
491 return Err(CapsuleCaptureError::ParticipantOutOfOrder {
492 participant_id: later.participant_id(),
493 });
494 }
495 }
496 if let Some(live) = view
497 .participants
498 .iter()
499 .find(|participant| matches!(participant, DigestParticipantView::Live { .. }))
500 {
501 return Err(CapsuleCaptureError::LiveParticipantSealed {
502 participant_id: live.participant_id(),
503 });
504 }
505 let participants: Vec<_> = view
506 .participants
507 .iter()
508 .map(ParticipantLifecycleView::storage_description)
509 .collect();
510 let encoded = encode_sealed_description(&participants, view.ledger_high_watermark)
511 .ok_or(CapsuleCaptureError::LengthOverflow)?;
512 Ok(SealedSections {
513 participants,
514 ledger_high_watermark: view.ledger_high_watermark,
515 encoded,
516 })
517}
518
519fn restore_sealed(sections: SealedSections) -> Result<SealedLifecycle, CapsuleRestoreError> {
521 let mut retired = Vec::with_capacity(sections.participants.len());
522 for participant in sections.participants {
523 match participant {
524 ParticipantLifecycleRestore::Retired(identity) => {
525 retired.push(identity.restore().map_err(CapsuleRestoreError::Sealed)?);
526 }
527 ParticipantLifecycleRestore::Live { identity, .. } => {
528 return Err(CapsuleRestoreError::SealedLiveParticipant {
529 participant_id: identity.participant_id,
530 });
531 }
532 }
533 }
534 Ok(SealedLifecycle {
535 retired,
536 ledger_high_watermark: sections.ledger_high_watermark,
537 })
538}
539
540fn decode_lifecycle(
543 input: &[u8],
544 at: &mut usize,
545 start: usize,
546 participants: &[u8],
547 conversation_id: ConversationId,
548) -> Result<LifecycleSections, CapsuleDecodeError> {
549 let frontiers = take_section(input, at, FRONTIERS_SECTION_TAG)?;
550 let closure = take_section(input, at, CLOSURE_SECTION_TAG)?;
551 let description = decode_description::<DIGEST_LEN>(participants, frontiers, closure)?;
552 if description.restore.frontiers.conversation_id != conversation_id {
553 return Err(CapsuleDecodeError::ForeignConversation {
554 tag: FRONTIERS_SECTION_TAG,
555 });
556 }
557 let encoded = input
558 .get(start..*at)
559 .ok_or(CapsuleDecodeError::LengthOverflow)?
560 .to_vec();
561 Ok(LifecycleSections {
562 description,
563 encoded,
564 })
565}
566
567fn take_section<'a>(
569 input: &'a [u8],
570 at: &mut usize,
571 expected: u16,
572) -> Result<&'a [u8], CapsuleDecodeError> {
573 if *at == input.len() {
574 return Err(CapsuleDecodeError::MissingSection { tag: expected });
575 }
576 let tag = take_u16(input, *at)?;
577 let len = take_u32(input, *at + 2)?;
578 let start = *at + SECTION_HEADER_LEN as usize;
579 let end = start
580 .checked_add(usize::try_from(len).map_err(|_| CapsuleDecodeError::LengthOverflow)?)
581 .ok_or(CapsuleDecodeError::LengthOverflow)?;
582 let body = input.get(start..end).ok_or(CapsuleDecodeError::Truncated {
583 required: end,
584 available: input.len(),
585 })?;
586 if tag != expected {
587 return Err(if SECTION_TAGS.contains(&tag) {
588 CapsuleDecodeError::SectionOutOfOrder { tag }
589 } else {
590 CapsuleDecodeError::UnknownSection { tag }
591 });
592 }
593 *at = end;
594 Ok(body)
595}
596
597#[derive(Debug, PartialEq, Eq)]
599pub struct RestoredLifecycle {
600 covered_log_sequence: u64,
601 restored_fenced_sources: BTreeSet<ParticipantId>,
602 conversation: ParticipantConversation,
603 lifecycle: Option<DigestConversationState>,
604 sealed: Option<SealedLifecycle>,
605}
606
607#[derive(Debug, PartialEq, Eq)]
610pub struct SealedLifecycle {
611 retired: Vec<DigestRetiredIdentity>,
612 ledger_high_watermark: u64,
613}
614
615impl SealedLifecycle {
616 #[must_use]
618 pub fn retired(&self) -> &[DigestRetiredIdentity] {
619 &self.retired
620 }
621
622 #[must_use]
624 pub const fn ledger_high_watermark(&self) -> u64 {
625 self.ledger_high_watermark
626 }
627
628 #[must_use]
630 pub fn into_parts(self) -> (Vec<DigestRetiredIdentity>, u64) {
631 (self.retired, self.ledger_high_watermark)
632 }
633}
634
635impl RestoredLifecycle {
636 #[doc(hidden)]
644 pub fn restore_checkpoint_observer_projection(
645 &self,
646 bytes: &[u8],
647 participant_id: ParticipantId,
648 ) -> Result<super::ObserverProgressProjection, super::OwnerCheckpointRestoreError> {
649 let (conversation_id, progress, source) =
650 super::checkpoint_codec::decode_observer_source(bytes)?;
651 if conversation_id != self.conversation.conversation_id()
652 || source.participant_id() != participant_id
653 {
654 return Err(super::OwnerCheckpointRestoreError::Field(
655 "observer_projection.capsule_association",
656 ));
657 }
658 let high_watermark = match (&self.lifecycle, &self.sealed) {
659 (Some(lifecycle), _) => lifecycle.frontiers().sequence().ledger().high_watermark(),
660 (None, Some(sealed)) => sealed.ledger_high_watermark,
661 (None, None) => {
662 return Err(super::OwnerCheckpointRestoreError::Field(
663 "observer_projection.lifecycle",
664 ));
665 }
666 };
667 if u128::from(progress) > u128::from(high_watermark) + 1 {
668 return Err(super::OwnerCheckpointRestoreError::Field(
669 "observer_projection.delivery_coverage",
670 ));
671 }
672 let generation = match (&self.lifecycle, &self.sealed) {
678 (Some(lifecycle), _) => match lifecycle
679 .participants()
680 .iter()
681 .find(|participant| participant.view().participant_id() == participant_id)
682 {
683 Some(super::RestoredParticipantLifecycle::Live { member, .. }) => {
684 Some(member.generation())
685 }
686 Some(super::RestoredParticipantLifecycle::Retired(retired)) => {
687 Some(retired.retired_generation())
688 }
689 None => {
690 return Err(super::OwnerCheckpointRestoreError::Field(
691 "observer_projection.participant",
692 ));
693 }
694 },
695 (None, Some(sealed)) => sealed
696 .retired
697 .iter()
698 .find(|retired| retired.participant_id() == participant_id)
699 .map(super::RetiredIdentity::retired_generation),
700 (None, None) => {
701 return Err(super::OwnerCheckpointRestoreError::Field(
702 "observer_projection.lifecycle",
703 ));
704 }
705 };
706 if let Some(generation) = generation
707 && source
708 .binding_epoch()
709 .is_some_and(|epoch| epoch.capability_generation.get() > generation.get())
710 {
711 return Err(super::OwnerCheckpointRestoreError::Field(
712 "observer_projection.binding_generation",
713 ));
714 }
715 Ok(super::ObserverProgressProjection::sourced(
716 conversation_id,
717 progress,
718 source,
719 ))
720 }
721
722 #[doc(hidden)]
733 pub fn restore_checkpoint_fenced_attach(
734 &mut self,
735 source: super::FencedAttachCommitRestore,
736 marker_record: &[u8],
737 ) -> Result<super::FencedAttachCommit, super::OwnerCheckpointRestoreError> {
738 use super::OwnerCheckpointRestoreError as Error;
739 if self
740 .restored_fenced_sources
741 .contains(&source.participant_id)
742 {
743 return Err(Error::Field("fenced_source.duplicate"));
744 }
745 let lifecycle = self
746 .lifecycle
747 .as_ref()
748 .ok_or(Error::Field("fenced_source.lifecycle"))?;
749 let origin = lifecycle
750 .participants()
751 .iter()
752 .find_map(|participant| match participant {
753 super::RestoredParticipantLifecycle::Live {
754 member,
755 binding_origin: Some(origin),
756 ..
757 } if member.participant_id() == source.participant_id => Some(*origin),
758 _ => None,
759 })
760 .ok_or(Error::Field("fenced_source.binding_origin"))?;
761 if origin.binding_epoch() != source.new_binding_epoch
762 || origin.recovered_marker()
763 != Some((source.marker_delivery_seq, source.prior_binding_epoch))
764 {
765 return Err(Error::Field("fenced_source.origin_association"));
766 }
767 let record = lifecycle
768 .frontiers()
769 .checkpoint_recovered_marker_record(origin)
770 .ok_or(Error::Field("fenced_source.historical_marker"))?;
771 if super::checkpoint_codec::encode_marker_source(&record) != marker_record {
772 return Err(Error::Field("fenced_source.marker_bytes"));
773 }
774 let restored = source
775 .restore_with_record(&record)
776 .map_err(Error::Provenance);
777 record.consume();
778 let restored = restored?;
779 self.restored_fenced_sources.insert(source.participant_id);
780 Ok(restored)
781 }
782
783 #[must_use]
786 pub const fn covered_log_sequence(&self) -> u64 {
787 self.covered_log_sequence
788 }
789
790 #[must_use]
792 pub fn into_parts(self) -> RestoredLifecycleParts {
793 RestoredLifecycleParts {
794 conversation: self.conversation,
795 lifecycle: self.lifecycle,
796 sealed: self.sealed,
797 }
798 }
799
800 #[must_use]
803 pub const fn sealed(&self) -> Option<&SealedLifecycle> {
804 self.sealed.as_ref()
805 }
806}
807
808#[derive(Debug)]
812#[non_exhaustive]
813pub struct RestoredLifecycleParts {
814 pub conversation: ParticipantConversation,
816 pub lifecycle: Option<DigestConversationState>,
820 pub sealed: Option<SealedLifecycle>,
823}
824
825#[derive(Clone, Copy, Debug, PartialEq, Eq)]
827#[non_exhaustive]
828pub enum CapsuleCaptureError {
829 ForeignConversation,
831 ParticipantOutOfOrder {
833 participant_id: ParticipantId,
835 },
836 Describe(ConversationDescribeError),
838 LengthOverflow,
840 SealedBesideLifecycle,
843 LiveParticipantSealed {
845 participant_id: ParticipantId,
847 },
848}
849
850#[derive(Clone, Copy, Debug, PartialEq, Eq)]
852#[non_exhaustive]
853pub enum CapsuleDecodeError {
854 Truncated {
856 required: usize,
858 available: usize,
860 },
861 InvalidMagic,
863 UnsupportedCodec {
865 major: u16,
867 minor: u16,
869 },
870 NonCanonicalLength {
873 declared_body_len: u32,
875 actual_body_len: usize,
877 },
878 MissingSection {
880 tag: u16,
882 },
883 SectionOutOfOrder {
885 tag: u16,
887 },
888 UnknownSection {
890 tag: u16,
892 },
893 NonCanonicalSectionLength {
895 tag: u16,
897 declared: u32,
899 },
900 NonCanonicalSection {
902 tag: u16,
904 },
905 ParticipantOutOfOrder {
907 participant_id: ParticipantId,
909 },
910 ForeignConversation {
912 tag: u16,
914 },
915 TrailingBytes {
917 trailing: usize,
919 },
920 LengthOverflow,
922}
923
924#[derive(Clone, Debug, PartialEq, Eq)]
926#[non_exhaustive]
927pub enum CapsuleRestoreError {
928 InconsistentConversation,
930 Lifecycle(ConversationStateRestoreError),
933 Sealed(StorageRestoreError),
935 SealedLiveParticipant {
937 participant_id: ParticipantId,
939 },
940}
941
942fn decode_conversation(body: &[u8]) -> Result<ConversationSection, CapsuleDecodeError> {
943 if body.len() != CONVERSATION_SECTION_LEN as usize {
944 return Err(CapsuleDecodeError::NonCanonicalSectionLength {
945 tag: CONVERSATION_SECTION_TAG,
946 declared: u32::try_from(body.len()).map_err(|_| CapsuleDecodeError::LengthOverflow)?,
947 });
948 }
949 let next_event_ordinal = take_u64(body, 0)?;
950 let flags = body.get(8).copied().ok_or(CapsuleDecodeError::Truncated {
951 required: 9,
952 available: body.len(),
953 })?;
954 if flags & !GENESIS_VALIDATED_FLAG != 0 {
955 return Err(CapsuleDecodeError::NonCanonicalSection {
956 tag: CONVERSATION_SECTION_TAG,
957 });
958 }
959 let section = ConversationSection {
960 next_event_ordinal,
961 genesis_validated: flags & GENESIS_VALIDATED_FLAG != 0,
962 };
963 if !section.is_consistent() {
964 return Err(CapsuleDecodeError::NonCanonicalSection {
965 tag: CONVERSATION_SECTION_TAG,
966 });
967 }
968 Ok(section)
969}
970
971fn take<const N: usize>(input: &[u8], start: usize) -> Result<[u8; N], CapsuleDecodeError> {
972 let end = start
973 .checked_add(N)
974 .ok_or(CapsuleDecodeError::LengthOverflow)?;
975 input
976 .get(start..end)
977 .ok_or(CapsuleDecodeError::Truncated {
978 required: end,
979 available: input.len(),
980 })?
981 .try_into()
982 .map_err(|_| CapsuleDecodeError::LengthOverflow)
983}
984
985fn take_u16(input: &[u8], start: usize) -> Result<u16, CapsuleDecodeError> {
986 take(input, start).map(u16::from_be_bytes)
987}
988
989fn take_u32(input: &[u8], start: usize) -> Result<u32, CapsuleDecodeError> {
990 take(input, start).map(u32::from_be_bytes)
991}
992
993fn take_u64(input: &[u8], start: usize) -> Result<u64, CapsuleDecodeError> {
994 take(input, start).map(u64::from_be_bytes)
995}
996
997#[doc(hidden)]
999#[derive(Clone, Debug, PartialEq, Eq)]
1000pub enum BindingOriginSourceError {
1001 Decode {
1003 participant_id: ParticipantId,
1005 error: CapsuleDecodeError,
1007 },
1008 Association {
1010 participant_id: ParticipantId,
1012 },
1013 Restore(CapsuleRestoreError),
1015}
1016
1017impl RestoredLifecycle {
1018 pub fn restore_cursor_episode(
1024 self,
1025 bytes: &[u8],
1026 observer_progress: u64,
1027 ) -> Result<RestoredLifecycleWithCursorEpisode, super::StorageRestoreError> {
1028 let lifecycle = self
1029 .lifecycle
1030 .as_ref()
1031 .ok_or(super::StorageRestoreError::CursorEpisode)?;
1032 let ClosureState::Owed { debt, .. } = lifecycle.closure() else {
1033 return Err(super::StorageRestoreError::CursorEpisode);
1034 };
1035 let episode = super::NonzeroDebtCursorEpisode::decode_checkpoint(
1036 bytes,
1037 lifecycle.frontiers(),
1038 debt,
1039 observer_progress,
1040 )?;
1041 Ok(RestoredLifecycleWithCursorEpisode {
1042 lifecycle: self,
1043 episode,
1044 })
1045 }
1046}
1047
1048#[derive(Debug)]
1050pub struct RestoredLifecycleWithCursorEpisode {
1051 lifecycle: RestoredLifecycle,
1052 episode: super::NonzeroDebtCursorEpisode,
1053}
1054impl RestoredLifecycleWithCursorEpisode {
1055 #[must_use]
1057 pub fn into_parts(self) -> (RestoredLifecycleParts, super::NonzeroDebtCursorEpisode) {
1058 (self.lifecycle.into_parts(), self.episode)
1059 }
1060}
1061
1062impl LifecycleCapsule {
1063 pub fn capture_with_binding_sources(
1071 view: &LifecycleView<'_>,
1072 sources: &[(ParticipantId, &[u8])],
1073 ) -> Result<Self, BindingOriginCaptureError> {
1074 let Some(lifecycle) = view.lifecycle.as_ref() else {
1075 if let Some(&(participant_id, _)) = sources.first() {
1076 return Err(BindingOriginCaptureError::Association { participant_id });
1077 }
1078 return Self::capture(view).map_err(BindingOriginCaptureError::Capture);
1079 };
1080 let mut remaining = sources.iter().peekable();
1081 let mut origins = Vec::new();
1082 for participant in &lifecycle.participants {
1083 match participant {
1084 DigestParticipantView::Retired(_) => origins.push(None),
1085 DigestParticipantView::Live {
1086 member,
1087 binding,
1088 binding_origin,
1089 ..
1090 } => {
1091 let participant_id = member.participant_id();
1092 let origin_required = !matches!(binding, super::BindingState::Detached)
1093 || member.latest_terminal().is_some()
1094 || binding_origin.is_some();
1095 let Some(&&(source_id, bytes)) = remaining.peek() else {
1096 if origin_required {
1097 return Err(BindingOriginCaptureError::Association { participant_id });
1098 }
1099 origins.push(None);
1100 continue;
1101 };
1102 if source_id != participant_id {
1103 if origin_required || source_id < participant_id {
1104 return Err(BindingOriginCaptureError::Association { participant_id });
1105 }
1106 origins.push(None);
1107 continue;
1108 }
1109 remaining.next();
1110 let origin = super::checkpoint_codec::decode_binding_origin_source(bytes)
1111 .map_err(|error| BindingOriginCaptureError::Decode {
1112 participant_id,
1113 error,
1114 })?;
1115 super::storage::validate_binding_origin(origin, member, **binding).map_err(
1116 |error| BindingOriginCaptureError::Validation {
1117 participant_id,
1118 error,
1119 },
1120 )?;
1121 if binding_origin.is_some_and(|expected| *expected != origin) {
1122 return Err(BindingOriginCaptureError::Association { participant_id });
1123 }
1124 origins.push(Some(origin));
1125 }
1126 }
1127 }
1128 if let Some(&(participant_id, _)) = remaining.next() {
1129 return Err(BindingOriginCaptureError::Association { participant_id });
1130 }
1131 let participants = lifecycle
1132 .participants
1133 .iter()
1134 .zip(&origins)
1135 .map(|(participant, origin)| match participant {
1136 DigestParticipantView::Live {
1137 member,
1138 binding,
1139 detach_cell,
1140 ..
1141 } => DigestParticipantView::Live {
1142 member,
1143 binding,
1144 detach_cell,
1145 binding_origin: origin.as_ref(),
1146 },
1147 DigestParticipantView::Retired(retired) => DigestParticipantView::Retired(retired),
1148 })
1149 .collect();
1150 let capsule = Self::capture(&LifecycleView {
1151 sealed: None,
1152 conversation: view.conversation,
1153 covered_log_sequence: view.covered_log_sequence,
1154 lifecycle: Some(ConversationLifecycleView {
1155 participants,
1156 frontiers: lifecycle.frontiers,
1157 retained_record_limit: lifecycle.retained_record_limit,
1158 closure: lifecycle.closure,
1159 }),
1160 })
1161 .map_err(BindingOriginCaptureError::Capture)?;
1162 let canonical = capsule.encode_canonical();
1163 capsule
1164 .restore()
1165 .map_err(BindingOriginCaptureError::Restore)?;
1166 Self::decode_canonical(&canonical).map_err(BindingOriginCaptureError::Canonical)
1167 }
1168}
1169
1170#[derive(Clone, Debug, PartialEq, Eq)]
1172pub enum BindingOriginCaptureError {
1173 Restore(CapsuleRestoreError),
1175 Canonical(CapsuleDecodeError),
1177 Decode {
1179 participant_id: ParticipantId,
1181 error: CapsuleDecodeError,
1183 },
1184 Association {
1186 participant_id: ParticipantId,
1188 },
1189 Validation {
1191 participant_id: ParticipantId,
1193 error: super::StorageRestoreError,
1195 },
1196 Capture(CapsuleCaptureError),
1198}