1use alloc::vec::Vec;
21
22use crate::wire::{ConversationId, ParticipantId};
23
24use super::checkpoint_codec::{
25 CLOSURE_SECTION_TAG, DigestDescription, FRONTIERS_SECTION_TAG, PARTICIPANTS_SECTION_TAG,
26 decode_description, encode_description,
27};
28use super::claim_frontier::ClaimFrontiers;
29use super::conversation::{ConversationGenesis, ParticipantConversation};
30use super::edge::ClosureState;
31use super::storage::{
32 ConversationDescribeError, ConversationStateRestoreError, ParticipantConversationState,
33 ParticipantLifecycleView, describe_conversation,
34};
35
36pub type DigestParticipantView<'a> =
38 ParticipantLifecycleView<'a, [u8; 32], [u8; 32], [u8; 32], [u8; 32]>;
39
40pub type DigestConversationState =
42 ParticipantConversationState<[u8; 32], [u8; 32], [u8; 32], [u8; 32]>;
43
44const CAPSULE_MAGIC: [u8; 4] = *b"LPCK";
45const CAPSULE_CODEC_MAJOR: u16 = 1;
46const CAPSULE_CODEC_MINOR: u16 = 0;
47const CAPSULE_HEADER_LEN: usize = 28;
49const SECTION_HEADER_LEN: u32 = 6;
51const CONVERSATION_SECTION_TAG: u16 = 1;
52const CONVERSATION_SECTION_LEN: u32 = 9;
54const GENESIS_VALIDATED_FLAG: u8 = 0b1;
55const CONVERSATION_FRAME_LEN: u32 = SECTION_HEADER_LEN + CONVERSATION_SECTION_LEN;
57const SECTION_TAGS: [u16; 4] = [
61 CONVERSATION_SECTION_TAG,
62 PARTICIPANTS_SECTION_TAG,
63 FRONTIERS_SECTION_TAG,
64 CLOSURE_SECTION_TAG,
65];
66const DIGEST_LEN: usize = 32;
69
70#[derive(Debug)]
73pub struct LifecycleView<'a> {
74 pub conversation: &'a ParticipantConversation,
76 pub lifecycle: Option<ConversationLifecycleView<'a>>,
79 pub covered_log_sequence: u64,
81}
82
83#[derive(Debug)]
85pub struct ConversationLifecycleView<'a> {
86 pub participants: Vec<DigestParticipantView<'a>>,
89 pub frontiers: &'a ClaimFrontiers,
91 pub retained_record_limit: u64,
93 pub closure: &'a ClosureState,
95}
96
97#[derive(Debug, PartialEq, Eq)]
101pub struct LifecycleCapsule {
102 conversation_id: ConversationId,
103 covered_log_sequence: u64,
104 conversation: ConversationSection,
105 lifecycle: Option<LifecycleSections>,
106 body_len: u32,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113struct ConversationSection {
114 next_event_ordinal: u64,
115 genesis_validated: bool,
116}
117
118impl ConversationSection {
119 const fn is_consistent(self) -> bool {
122 self.genesis_validated == (self.next_event_ordinal > 0)
123 }
124}
125
126#[derive(Debug, PartialEq, Eq)]
129struct LifecycleSections {
130 description: DigestDescription<DIGEST_LEN>,
131 encoded: Vec<u8>,
132}
133
134impl LifecycleCapsule {
135 pub fn capture(view: &LifecycleView<'_>) -> Result<Self, CapsuleCaptureError> {
144 let conversation_id = view.conversation.conversation_id();
145 let lifecycle = view
146 .lifecycle
147 .as_ref()
148 .map(|lifecycle| capture_lifecycle(conversation_id, lifecycle))
149 .transpose()?;
150 let lifecycle_len = lifecycle
151 .as_ref()
152 .map_or(0, |sections| sections.encoded.len());
153 let body_len = u32::try_from(lifecycle_len)
154 .map_err(|_| CapsuleCaptureError::LengthOverflow)?
155 .checked_add(CONVERSATION_FRAME_LEN)
156 .ok_or(CapsuleCaptureError::LengthOverflow)?;
157 Ok(Self {
158 conversation_id,
159 covered_log_sequence: view.covered_log_sequence,
160 conversation: ConversationSection {
161 next_event_ordinal: view.conversation.next_event_ordinal(),
162 genesis_validated: view.conversation.genesis_validated(),
163 },
164 lifecycle,
165 body_len,
166 })
167 }
168
169 #[must_use]
171 pub const fn conversation_id(&self) -> ConversationId {
172 self.conversation_id
173 }
174
175 #[must_use]
177 pub const fn covered_log_sequence(&self) -> u64 {
178 self.covered_log_sequence
179 }
180
181 #[must_use]
183 pub fn encode_canonical(&self) -> Vec<u8> {
184 let mut encoded = Vec::with_capacity(CAPSULE_HEADER_LEN + self.body_len as usize);
185 encoded.extend_from_slice(&CAPSULE_MAGIC);
186 encoded.extend_from_slice(&CAPSULE_CODEC_MAJOR.to_be_bytes());
187 encoded.extend_from_slice(&CAPSULE_CODEC_MINOR.to_be_bytes());
188 encoded.extend_from_slice(&self.conversation_id.to_be_bytes());
189 encoded.extend_from_slice(&self.covered_log_sequence.to_be_bytes());
190 encoded.extend_from_slice(&self.body_len.to_be_bytes());
191 encoded.extend_from_slice(&CONVERSATION_SECTION_TAG.to_be_bytes());
192 encoded.extend_from_slice(&CONVERSATION_SECTION_LEN.to_be_bytes());
193 encoded.extend_from_slice(&self.conversation.next_event_ordinal.to_be_bytes());
194 encoded.push(if self.conversation.genesis_validated {
195 GENESIS_VALIDATED_FLAG
196 } else {
197 0
198 });
199 if let Some(sections) = &self.lifecycle {
200 encoded.extend_from_slice(§ions.encoded);
201 }
202 encoded
203 }
204
205 pub fn decode_canonical(input: &[u8]) -> Result<Self, CapsuleDecodeError> {
215 if input.len() < CAPSULE_HEADER_LEN {
216 return Err(CapsuleDecodeError::Truncated {
217 required: CAPSULE_HEADER_LEN,
218 available: input.len(),
219 });
220 }
221 if input.get(0..4) != Some(CAPSULE_MAGIC.as_slice()) {
222 return Err(CapsuleDecodeError::InvalidMagic);
223 }
224 let major = take_u16(input, 4)?;
225 let minor = take_u16(input, 6)?;
226 if (major, minor) != (CAPSULE_CODEC_MAJOR, CAPSULE_CODEC_MINOR) {
227 return Err(CapsuleDecodeError::UnsupportedCodec { major, minor });
228 }
229 let conversation_id = take_u64(input, 8)?;
230 let covered_log_sequence = take_u64(input, 16)?;
231 let declared_body_len = take_u32(input, 24)?;
232 let actual_body_len = input.len() - CAPSULE_HEADER_LEN;
233 if usize::try_from(declared_body_len).map_err(|_| CapsuleDecodeError::LengthOverflow)?
234 != actual_body_len
235 {
236 return Err(CapsuleDecodeError::NonCanonicalLength {
237 declared_body_len,
238 actual_body_len,
239 });
240 }
241 let mut at = CAPSULE_HEADER_LEN;
242 let conversation =
243 decode_conversation(take_section(input, &mut at, CONVERSATION_SECTION_TAG)?)?;
244 let lifecycle = if input.len() - at < SECTION_HEADER_LEN as usize {
245 None
246 } else {
247 Some(decode_lifecycle(input, &mut at, conversation_id)?)
248 };
249 if at != input.len() {
250 return Err(CapsuleDecodeError::TrailingBytes {
251 trailing: input.len() - at,
252 });
253 }
254 Ok(Self {
255 conversation_id,
256 covered_log_sequence,
257 conversation,
258 lifecycle,
259 body_len: declared_body_len,
260 })
261 }
262
263 pub fn restore(self) -> Result<RestoredLifecycle, CapsuleRestoreError> {
275 if !self.conversation.is_consistent() {
276 return Err(CapsuleRestoreError::InconsistentConversation);
277 }
278 let lifecycle = self
279 .lifecycle
280 .map(|sections| sections.description.into_state())
281 .transpose()
282 .map_err(CapsuleRestoreError::Lifecycle)?;
283 Ok(RestoredLifecycle {
284 covered_log_sequence: self.covered_log_sequence,
285 conversation: ParticipantConversation::from_checkpoint(
286 ConversationGenesis::new(self.conversation_id),
287 self.conversation.next_event_ordinal,
288 self.conversation.genesis_validated,
289 ),
290 lifecycle,
291 })
292 }
293}
294
295fn capture_lifecycle(
298 conversation_id: ConversationId,
299 view: &ConversationLifecycleView<'_>,
300) -> Result<LifecycleSections, CapsuleCaptureError> {
301 if view.frontiers.conversation_id() != conversation_id {
302 return Err(CapsuleCaptureError::ForeignConversation);
303 }
304 for pair in view.participants.windows(2) {
305 if let [earlier, later] = pair
306 && earlier.participant_id() >= later.participant_id()
307 {
308 return Err(CapsuleCaptureError::ParticipantOutOfOrder {
309 participant_id: later.participant_id(),
310 });
311 }
312 }
313 let description = describe_conversation(
314 &view.participants,
315 view.frontiers,
316 view.retained_record_limit,
317 *view.closure,
318 )
319 .map_err(CapsuleCaptureError::Describe)?;
320 let encoded = encode_description(&description).ok_or(CapsuleCaptureError::LengthOverflow)?;
321 Ok(LifecycleSections {
322 description,
323 encoded,
324 })
325}
326
327fn decode_lifecycle(
330 input: &[u8],
331 at: &mut usize,
332 conversation_id: ConversationId,
333) -> Result<LifecycleSections, CapsuleDecodeError> {
334 let start = *at;
335 let participants = take_section(input, at, PARTICIPANTS_SECTION_TAG)?;
336 let frontiers = take_section(input, at, FRONTIERS_SECTION_TAG)?;
337 let closure = take_section(input, at, CLOSURE_SECTION_TAG)?;
338 let description = decode_description::<DIGEST_LEN>(participants, frontiers, closure)?;
339 if description.restore.frontiers.conversation_id != conversation_id {
340 return Err(CapsuleDecodeError::ForeignConversation {
341 tag: FRONTIERS_SECTION_TAG,
342 });
343 }
344 let encoded = input
345 .get(start..*at)
346 .ok_or(CapsuleDecodeError::LengthOverflow)?
347 .to_vec();
348 Ok(LifecycleSections {
349 description,
350 encoded,
351 })
352}
353
354fn take_section<'a>(
356 input: &'a [u8],
357 at: &mut usize,
358 expected: u16,
359) -> Result<&'a [u8], CapsuleDecodeError> {
360 if *at == input.len() {
361 return Err(CapsuleDecodeError::MissingSection { tag: expected });
362 }
363 let tag = take_u16(input, *at)?;
364 let len = take_u32(input, *at + 2)?;
365 let start = *at + SECTION_HEADER_LEN as usize;
366 let end = start
367 .checked_add(usize::try_from(len).map_err(|_| CapsuleDecodeError::LengthOverflow)?)
368 .ok_or(CapsuleDecodeError::LengthOverflow)?;
369 let body = input.get(start..end).ok_or(CapsuleDecodeError::Truncated {
370 required: end,
371 available: input.len(),
372 })?;
373 if tag != expected {
374 return Err(if SECTION_TAGS.contains(&tag) {
375 CapsuleDecodeError::SectionOutOfOrder { tag }
376 } else {
377 CapsuleDecodeError::UnknownSection { tag }
378 });
379 }
380 *at = end;
381 Ok(body)
382}
383
384#[derive(Debug, PartialEq, Eq)]
386pub struct RestoredLifecycle {
387 covered_log_sequence: u64,
388 conversation: ParticipantConversation,
389 lifecycle: Option<DigestConversationState>,
390}
391
392impl RestoredLifecycle {
393 #[must_use]
396 pub const fn covered_log_sequence(&self) -> u64 {
397 self.covered_log_sequence
398 }
399
400 #[must_use]
402 pub fn into_parts(self) -> RestoredLifecycleParts {
403 RestoredLifecycleParts {
404 conversation: self.conversation,
405 lifecycle: self.lifecycle,
406 }
407 }
408}
409
410#[derive(Debug)]
414#[non_exhaustive]
415pub struct RestoredLifecycleParts {
416 pub conversation: ParticipantConversation,
418 pub lifecycle: Option<DigestConversationState>,
422}
423
424#[derive(Clone, Copy, Debug, PartialEq, Eq)]
426#[non_exhaustive]
427pub enum CapsuleCaptureError {
428 ForeignConversation,
430 ParticipantOutOfOrder {
432 participant_id: ParticipantId,
434 },
435 Describe(ConversationDescribeError),
437 LengthOverflow,
439}
440
441#[derive(Clone, Copy, Debug, PartialEq, Eq)]
443#[non_exhaustive]
444pub enum CapsuleDecodeError {
445 Truncated {
447 required: usize,
449 available: usize,
451 },
452 InvalidMagic,
454 UnsupportedCodec {
456 major: u16,
458 minor: u16,
460 },
461 NonCanonicalLength {
464 declared_body_len: u32,
466 actual_body_len: usize,
468 },
469 MissingSection {
471 tag: u16,
473 },
474 SectionOutOfOrder {
476 tag: u16,
478 },
479 UnknownSection {
481 tag: u16,
483 },
484 NonCanonicalSectionLength {
486 tag: u16,
488 declared: u32,
490 },
491 NonCanonicalSection {
493 tag: u16,
495 },
496 ParticipantOutOfOrder {
498 participant_id: ParticipantId,
500 },
501 ForeignConversation {
503 tag: u16,
505 },
506 TrailingBytes {
508 trailing: usize,
510 },
511 LengthOverflow,
513}
514
515#[derive(Clone, Debug, PartialEq, Eq)]
517#[non_exhaustive]
518pub enum CapsuleRestoreError {
519 InconsistentConversation,
521 Lifecycle(ConversationStateRestoreError),
524}
525
526fn decode_conversation(body: &[u8]) -> Result<ConversationSection, CapsuleDecodeError> {
527 if body.len() != CONVERSATION_SECTION_LEN as usize {
528 return Err(CapsuleDecodeError::NonCanonicalSectionLength {
529 tag: CONVERSATION_SECTION_TAG,
530 declared: u32::try_from(body.len()).map_err(|_| CapsuleDecodeError::LengthOverflow)?,
531 });
532 }
533 let next_event_ordinal = take_u64(body, 0)?;
534 let flags = body.get(8).copied().ok_or(CapsuleDecodeError::Truncated {
535 required: 9,
536 available: body.len(),
537 })?;
538 if flags & !GENESIS_VALIDATED_FLAG != 0 {
539 return Err(CapsuleDecodeError::NonCanonicalSection {
540 tag: CONVERSATION_SECTION_TAG,
541 });
542 }
543 let section = ConversationSection {
544 next_event_ordinal,
545 genesis_validated: flags & GENESIS_VALIDATED_FLAG != 0,
546 };
547 if !section.is_consistent() {
548 return Err(CapsuleDecodeError::NonCanonicalSection {
549 tag: CONVERSATION_SECTION_TAG,
550 });
551 }
552 Ok(section)
553}
554
555fn take<const N: usize>(input: &[u8], start: usize) -> Result<[u8; N], CapsuleDecodeError> {
556 let end = start
557 .checked_add(N)
558 .ok_or(CapsuleDecodeError::LengthOverflow)?;
559 input
560 .get(start..end)
561 .ok_or(CapsuleDecodeError::Truncated {
562 required: end,
563 available: input.len(),
564 })?
565 .try_into()
566 .map_err(|_| CapsuleDecodeError::LengthOverflow)
567}
568
569fn take_u16(input: &[u8], start: usize) -> Result<u16, CapsuleDecodeError> {
570 take(input, start).map(u16::from_be_bytes)
571}
572
573fn take_u32(input: &[u8], start: usize) -> Result<u32, CapsuleDecodeError> {
574 take(input, start).map(u32::from_be_bytes)
575}
576
577fn take_u64(input: &[u8], start: usize) -> Result<u64, CapsuleDecodeError> {
578 take(input, start).map(u64::from_be_bytes)
579}