use alloc::vec::Vec;
use crate::wire::{ConversationId, ParticipantId};
use super::checkpoint_codec::{
CLOSURE_SECTION_TAG, DigestDescription, FRONTIERS_SECTION_TAG, PARTICIPANTS_SECTION_TAG,
decode_description, encode_description,
};
use super::claim_frontier::ClaimFrontiers;
use super::conversation::{ConversationGenesis, ParticipantConversation};
use super::edge::ClosureState;
use super::storage::{
ConversationDescribeError, ConversationStateRestoreError, ParticipantConversationState,
ParticipantLifecycleView, describe_conversation,
};
pub type DigestParticipantView<'a> =
ParticipantLifecycleView<'a, [u8; 32], [u8; 32], [u8; 32], [u8; 32]>;
pub type DigestConversationState =
ParticipantConversationState<[u8; 32], [u8; 32], [u8; 32], [u8; 32]>;
const CAPSULE_MAGIC: [u8; 4] = *b"LPCK";
const CAPSULE_CODEC_MAJOR: u16 = 1;
const CAPSULE_CODEC_MINOR: u16 = 0;
const CAPSULE_HEADER_LEN: usize = 28;
const SECTION_HEADER_LEN: u32 = 6;
const CONVERSATION_SECTION_TAG: u16 = 1;
const CONVERSATION_SECTION_LEN: u32 = 9;
const GENESIS_VALIDATED_FLAG: u8 = 0b1;
const CONVERSATION_FRAME_LEN: u32 = SECTION_HEADER_LEN + CONVERSATION_SECTION_LEN;
const SECTION_TAGS: [u16; 4] = [
CONVERSATION_SECTION_TAG,
PARTICIPANTS_SECTION_TAG,
FRONTIERS_SECTION_TAG,
CLOSURE_SECTION_TAG,
];
const DIGEST_LEN: usize = 32;
#[derive(Debug)]
pub struct LifecycleView<'a> {
pub conversation: &'a ParticipantConversation,
pub lifecycle: Option<ConversationLifecycleView<'a>>,
pub covered_log_sequence: u64,
}
#[derive(Debug)]
pub struct ConversationLifecycleView<'a> {
pub participants: Vec<DigestParticipantView<'a>>,
pub frontiers: &'a ClaimFrontiers,
pub retained_record_limit: u64,
pub closure: &'a ClosureState,
}
#[derive(Debug, PartialEq, Eq)]
pub struct LifecycleCapsule {
conversation_id: ConversationId,
covered_log_sequence: u64,
conversation: ConversationSection,
lifecycle: Option<LifecycleSections>,
body_len: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ConversationSection {
next_event_ordinal: u64,
genesis_validated: bool,
}
impl ConversationSection {
const fn is_consistent(self) -> bool {
self.genesis_validated == (self.next_event_ordinal > 0)
}
}
#[derive(Debug, PartialEq, Eq)]
struct LifecycleSections {
description: DigestDescription<DIGEST_LEN>,
encoded: Vec<u8>,
}
impl LifecycleCapsule {
pub fn capture(view: &LifecycleView<'_>) -> Result<Self, CapsuleCaptureError> {
let conversation_id = view.conversation.conversation_id();
let lifecycle = view
.lifecycle
.as_ref()
.map(|lifecycle| capture_lifecycle(conversation_id, lifecycle))
.transpose()?;
let lifecycle_len = lifecycle
.as_ref()
.map_or(0, |sections| sections.encoded.len());
let body_len = u32::try_from(lifecycle_len)
.map_err(|_| CapsuleCaptureError::LengthOverflow)?
.checked_add(CONVERSATION_FRAME_LEN)
.ok_or(CapsuleCaptureError::LengthOverflow)?;
Ok(Self {
conversation_id,
covered_log_sequence: view.covered_log_sequence,
conversation: ConversationSection {
next_event_ordinal: view.conversation.next_event_ordinal(),
genesis_validated: view.conversation.genesis_validated(),
},
lifecycle,
body_len,
})
}
#[must_use]
pub const fn conversation_id(&self) -> ConversationId {
self.conversation_id
}
#[must_use]
pub const fn covered_log_sequence(&self) -> u64 {
self.covered_log_sequence
}
#[must_use]
pub fn encode_canonical(&self) -> Vec<u8> {
let mut encoded = Vec::with_capacity(CAPSULE_HEADER_LEN + self.body_len as usize);
encoded.extend_from_slice(&CAPSULE_MAGIC);
encoded.extend_from_slice(&CAPSULE_CODEC_MAJOR.to_be_bytes());
encoded.extend_from_slice(&CAPSULE_CODEC_MINOR.to_be_bytes());
encoded.extend_from_slice(&self.conversation_id.to_be_bytes());
encoded.extend_from_slice(&self.covered_log_sequence.to_be_bytes());
encoded.extend_from_slice(&self.body_len.to_be_bytes());
encoded.extend_from_slice(&CONVERSATION_SECTION_TAG.to_be_bytes());
encoded.extend_from_slice(&CONVERSATION_SECTION_LEN.to_be_bytes());
encoded.extend_from_slice(&self.conversation.next_event_ordinal.to_be_bytes());
encoded.push(if self.conversation.genesis_validated {
GENESIS_VALIDATED_FLAG
} else {
0
});
if let Some(sections) = &self.lifecycle {
encoded.extend_from_slice(§ions.encoded);
}
encoded
}
pub fn decode_canonical(input: &[u8]) -> Result<Self, CapsuleDecodeError> {
if input.len() < CAPSULE_HEADER_LEN {
return Err(CapsuleDecodeError::Truncated {
required: CAPSULE_HEADER_LEN,
available: input.len(),
});
}
if input.get(0..4) != Some(CAPSULE_MAGIC.as_slice()) {
return Err(CapsuleDecodeError::InvalidMagic);
}
let major = take_u16(input, 4)?;
let minor = take_u16(input, 6)?;
if (major, minor) != (CAPSULE_CODEC_MAJOR, CAPSULE_CODEC_MINOR) {
return Err(CapsuleDecodeError::UnsupportedCodec { major, minor });
}
let conversation_id = take_u64(input, 8)?;
let covered_log_sequence = take_u64(input, 16)?;
let declared_body_len = take_u32(input, 24)?;
let actual_body_len = input.len() - CAPSULE_HEADER_LEN;
if usize::try_from(declared_body_len).map_err(|_| CapsuleDecodeError::LengthOverflow)?
!= actual_body_len
{
return Err(CapsuleDecodeError::NonCanonicalLength {
declared_body_len,
actual_body_len,
});
}
let mut at = CAPSULE_HEADER_LEN;
let conversation =
decode_conversation(take_section(input, &mut at, CONVERSATION_SECTION_TAG)?)?;
let lifecycle = if input.len() - at < SECTION_HEADER_LEN as usize {
None
} else {
Some(decode_lifecycle(input, &mut at, conversation_id)?)
};
if at != input.len() {
return Err(CapsuleDecodeError::TrailingBytes {
trailing: input.len() - at,
});
}
Ok(Self {
conversation_id,
covered_log_sequence,
conversation,
lifecycle,
body_len: declared_body_len,
})
}
pub fn restore(self) -> Result<RestoredLifecycle, CapsuleRestoreError> {
if !self.conversation.is_consistent() {
return Err(CapsuleRestoreError::InconsistentConversation);
}
let lifecycle = self
.lifecycle
.map(|sections| sections.description.into_state())
.transpose()
.map_err(CapsuleRestoreError::Lifecycle)?;
Ok(RestoredLifecycle {
covered_log_sequence: self.covered_log_sequence,
conversation: ParticipantConversation::from_checkpoint(
ConversationGenesis::new(self.conversation_id),
self.conversation.next_event_ordinal,
self.conversation.genesis_validated,
),
lifecycle,
})
}
}
fn capture_lifecycle(
conversation_id: ConversationId,
view: &ConversationLifecycleView<'_>,
) -> Result<LifecycleSections, CapsuleCaptureError> {
if view.frontiers.conversation_id() != conversation_id {
return Err(CapsuleCaptureError::ForeignConversation);
}
for pair in view.participants.windows(2) {
if let [earlier, later] = pair
&& earlier.participant_id() >= later.participant_id()
{
return Err(CapsuleCaptureError::ParticipantOutOfOrder {
participant_id: later.participant_id(),
});
}
}
let description = describe_conversation(
&view.participants,
view.frontiers,
view.retained_record_limit,
*view.closure,
)
.map_err(CapsuleCaptureError::Describe)?;
let encoded = encode_description(&description).ok_or(CapsuleCaptureError::LengthOverflow)?;
Ok(LifecycleSections {
description,
encoded,
})
}
fn decode_lifecycle(
input: &[u8],
at: &mut usize,
conversation_id: ConversationId,
) -> Result<LifecycleSections, CapsuleDecodeError> {
let start = *at;
let participants = take_section(input, at, PARTICIPANTS_SECTION_TAG)?;
let frontiers = take_section(input, at, FRONTIERS_SECTION_TAG)?;
let closure = take_section(input, at, CLOSURE_SECTION_TAG)?;
let description = decode_description::<DIGEST_LEN>(participants, frontiers, closure)?;
if description.restore.frontiers.conversation_id != conversation_id {
return Err(CapsuleDecodeError::ForeignConversation {
tag: FRONTIERS_SECTION_TAG,
});
}
let encoded = input
.get(start..*at)
.ok_or(CapsuleDecodeError::LengthOverflow)?
.to_vec();
Ok(LifecycleSections {
description,
encoded,
})
}
fn take_section<'a>(
input: &'a [u8],
at: &mut usize,
expected: u16,
) -> Result<&'a [u8], CapsuleDecodeError> {
if *at == input.len() {
return Err(CapsuleDecodeError::MissingSection { tag: expected });
}
let tag = take_u16(input, *at)?;
let len = take_u32(input, *at + 2)?;
let start = *at + SECTION_HEADER_LEN as usize;
let end = start
.checked_add(usize::try_from(len).map_err(|_| CapsuleDecodeError::LengthOverflow)?)
.ok_or(CapsuleDecodeError::LengthOverflow)?;
let body = input.get(start..end).ok_or(CapsuleDecodeError::Truncated {
required: end,
available: input.len(),
})?;
if tag != expected {
return Err(if SECTION_TAGS.contains(&tag) {
CapsuleDecodeError::SectionOutOfOrder { tag }
} else {
CapsuleDecodeError::UnknownSection { tag }
});
}
*at = end;
Ok(body)
}
#[derive(Debug, PartialEq, Eq)]
pub struct RestoredLifecycle {
covered_log_sequence: u64,
conversation: ParticipantConversation,
lifecycle: Option<DigestConversationState>,
}
impl RestoredLifecycle {
#[must_use]
pub const fn covered_log_sequence(&self) -> u64 {
self.covered_log_sequence
}
#[must_use]
pub fn into_parts(self) -> RestoredLifecycleParts {
RestoredLifecycleParts {
conversation: self.conversation,
lifecycle: self.lifecycle,
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct RestoredLifecycleParts {
pub conversation: ParticipantConversation,
pub lifecycle: Option<DigestConversationState>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CapsuleCaptureError {
ForeignConversation,
ParticipantOutOfOrder {
participant_id: ParticipantId,
},
Describe(ConversationDescribeError),
LengthOverflow,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CapsuleDecodeError {
Truncated {
required: usize,
available: usize,
},
InvalidMagic,
UnsupportedCodec {
major: u16,
minor: u16,
},
NonCanonicalLength {
declared_body_len: u32,
actual_body_len: usize,
},
MissingSection {
tag: u16,
},
SectionOutOfOrder {
tag: u16,
},
UnknownSection {
tag: u16,
},
NonCanonicalSectionLength {
tag: u16,
declared: u32,
},
NonCanonicalSection {
tag: u16,
},
ParticipantOutOfOrder {
participant_id: ParticipantId,
},
ForeignConversation {
tag: u16,
},
TrailingBytes {
trailing: usize,
},
LengthOverflow,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CapsuleRestoreError {
InconsistentConversation,
Lifecycle(ConversationStateRestoreError),
}
fn decode_conversation(body: &[u8]) -> Result<ConversationSection, CapsuleDecodeError> {
if body.len() != CONVERSATION_SECTION_LEN as usize {
return Err(CapsuleDecodeError::NonCanonicalSectionLength {
tag: CONVERSATION_SECTION_TAG,
declared: u32::try_from(body.len()).map_err(|_| CapsuleDecodeError::LengthOverflow)?,
});
}
let next_event_ordinal = take_u64(body, 0)?;
let flags = body.get(8).copied().ok_or(CapsuleDecodeError::Truncated {
required: 9,
available: body.len(),
})?;
if flags & !GENESIS_VALIDATED_FLAG != 0 {
return Err(CapsuleDecodeError::NonCanonicalSection {
tag: CONVERSATION_SECTION_TAG,
});
}
let section = ConversationSection {
next_event_ordinal,
genesis_validated: flags & GENESIS_VALIDATED_FLAG != 0,
};
if !section.is_consistent() {
return Err(CapsuleDecodeError::NonCanonicalSection {
tag: CONVERSATION_SECTION_TAG,
});
}
Ok(section)
}
fn take<const N: usize>(input: &[u8], start: usize) -> Result<[u8; N], CapsuleDecodeError> {
let end = start
.checked_add(N)
.ok_or(CapsuleDecodeError::LengthOverflow)?;
input
.get(start..end)
.ok_or(CapsuleDecodeError::Truncated {
required: end,
available: input.len(),
})?
.try_into()
.map_err(|_| CapsuleDecodeError::LengthOverflow)
}
fn take_u16(input: &[u8], start: usize) -> Result<u16, CapsuleDecodeError> {
take(input, start).map(u16::from_be_bytes)
}
fn take_u32(input: &[u8], start: usize) -> Result<u32, CapsuleDecodeError> {
take(input, start).map(u32::from_be_bytes)
}
fn take_u64(input: &[u8], start: usize) -> Result<u64, CapsuleDecodeError> {
take(input, start).map(u64::from_be_bytes)
}