liminal_protocol/lifecycle/operations/live_frontier/
checkpoint_restore.rs1use alloc::vec::Vec;
4
5use crate::algebra::{ResourceVector, WideResourceVector};
6use crate::lifecycle::{
7 ClosureAccounting, OrdinaryProjectionError, ParticipantConversation, RestoredLifecycleParts,
8 RestoredParticipantLifecycle, RetainedRecordCharge,
9};
10
11use super::LiveFrontierOwner;
12
13pub type CheckpointParticipant =
15 RestoredParticipantLifecycle<[u8; 32], [u8; 32], [u8; 32], [u8; 32]>;
16
17#[derive(Debug)]
19#[non_exhaustive]
20pub struct RestoredLiveFrontierOwner {
21 pub conversation: ParticipantConversation,
23 pub participants: Vec<CheckpointParticipant>,
25 pub owner: LiveFrontierOwner,
27 pub cursor_episode: Option<crate::lifecycle::NonzeroDebtCursorEpisode>,
29 pub(in crate::lifecycle) observer_progress: u64,
30}
31
32#[derive(Debug, PartialEq, Eq)]
34pub enum LiveFrontierRestoreError {
35 MissingLifecycle,
37 MissingClosureAccounting,
39 MissingRetainedCharges,
41 MissingCursorEpisode,
43 UnexpectedCursorEpisode,
45 AccountingCodec(crate::lifecycle::FrontierAccountingDecodeError),
47 RetainedChargeCodec(crate::lifecycle::CapsuleDecodeError),
49 CursorEpisode(crate::lifecycle::StorageRestoreError),
51 CursorEpisodeEncoding(crate::lifecycle::CursorFactEncodeError),
53 ForeignConversation,
55 ClosureMismatch,
57 RetainedRecordLimit,
59 Accounting(OrdinaryProjectionError),
61}
62
63impl LiveFrontierOwner {
64 fn restore_checkpoint_parts(
73 parts: RestoredLifecycleParts,
74 accounting: ClosureAccounting,
75 charges: Vec<RetainedRecordCharge>,
76 cursor_episode: Option<crate::lifecycle::NonzeroDebtCursorEpisode>,
77 saved: FrontierCheckpointSource<'_>,
78 ) -> Result<RestoredLiveFrontierOwner, LiveFrontierRestoreError> {
79 let lifecycle = parts
80 .lifecycle
81 .ok_or(LiveFrontierRestoreError::MissingLifecycle)?;
82 if lifecycle.frontiers().conversation_id() != parts.conversation.conversation_id() {
83 return Err(LiveFrontierRestoreError::ForeignConversation);
84 }
85 if lifecycle.closure() != accounting.state() {
86 return Err(LiveFrontierRestoreError::ClosureMismatch);
87 }
88 if lifecycle.frontiers().retained_records().len() as u128
89 > u128::from(saved.retained_record_limit)
90 {
91 return Err(LiveFrontierRestoreError::RetainedRecordLimit);
92 }
93 validate_saved_accounting(
94 lifecycle.frontiers(),
95 &accounting,
96 &charges,
97 saved.marker_max,
98 )
99 .map_err(LiveFrontierRestoreError::Accounting)?;
100 let (participants, frontiers, _) = lifecycle.into_parts();
101 Ok(RestoredLiveFrontierOwner {
102 conversation: parts.conversation,
103 participants,
104 cursor_episode,
105 observer_progress: saved.observer_progress,
106 owner: Self {
107 frontiers,
108 closure_accounting: accounting,
109 retained_charges: charges,
110 retained_record_limit: saved.retained_record_limit,
111 },
112 })
113 }
114}
115
116fn validate_saved_accounting(
117 frontiers: &crate::lifecycle::ClaimFrontiers,
118 accounting: &ClosureAccounting,
119 charges: &[RetainedRecordCharge],
120 marker_max: ResourceVector,
121) -> Result<(), OrdinaryProjectionError> {
122 let records = frontiers.retained_records();
123 let high_end = u128::from(frontiers.sequence().ledger().high_watermark()) + 1;
124 if high_end.checked_sub(frontiers.retained_floor()) != Some(records.len() as u128)
125 || records.len() != charges.len()
126 {
127 return Err(OrdinaryProjectionError::RetainedSuffix);
128 }
129 let mut retained = WideResourceVector::default();
130 for (index, (record, charge)) in records.iter().zip(charges).enumerate() {
131 if u128::from(record.delivery_seq) != frontiers.retained_floor() + index as u128
132 || charge.delivery_seq() != record.delivery_seq
133 || charge.admission_order() != record.admission_order
134 {
135 return Err(OrdinaryProjectionError::RetainedChargeKey {
136 index: u64::try_from(index).map_err(|_| OrdinaryProjectionError::RetainedSuffix)?,
137 });
138 }
139 let resource = charge.encoded_charge();
140 if resource.entries != 1 {
141 return Err(OrdinaryProjectionError::RetainedEntryCharge {
142 delivery_seq: charge.delivery_seq(),
143 entries: resource.entries,
144 });
145 }
146 retained =
147 super::super::ordinary_record_projection::checked_add_resource(retained, resource)?;
148 }
149 let pending = frontiers.pending_marker_candidate_count();
150 let retained_credits = u64::try_from(frontiers.retained_marker_records().len())
151 .map_err(|_| OrdinaryProjectionError::MarkerCredit)?;
152 let credits = retained_credits
153 .checked_add(pending)
154 .ok_or(OrdinaryProjectionError::MarkerCredit)?;
155 if credits != accounting.marker_capacity_credits() {
156 return Err(OrdinaryProjectionError::MarkerCreditAccounting {
157 derived: credits,
158 stored: accounting.marker_capacity_credits(),
159 });
160 }
161 let anchors = frontiers
162 .unaccepted_marker_anchor_count()
163 .checked_add(pending)
164 .ok_or(OrdinaryProjectionError::MarkerAnchor)?;
165 if anchors != accounting.marker_anchors() {
166 return Err(OrdinaryProjectionError::MarkerAnchorAccounting {
167 derived: anchors,
168 stored: accounting.marker_anchors(),
169 });
170 }
171 let baseline = super::super::ordinary_record_projection::retained_baseline_wide(
172 retained,
173 frontiers.identity_slot_limit(),
174 retained_credits,
175 marker_max,
176 )?;
177 if baseline != accounting.baseline() {
178 return Err(OrdinaryProjectionError::BaselineAccounting {
179 derived: baseline,
180 stored: accounting.baseline(),
181 });
182 }
183 Ok(())
184}
185
186impl LiveFrontierOwner {
187 pub fn restore_checkpoint(
194 parts: RestoredLifecycleParts,
195 saved: FrontierCheckpointSource<'_>,
196 ) -> Result<RestoredLiveFrontierOwner, LiveFrontierRestoreError> {
197 let lifecycle = parts
198 .lifecycle
199 .as_ref()
200 .ok_or(LiveFrontierRestoreError::MissingLifecycle)?;
201 let accounting = Self::decode_closure_accounting_source(
202 lifecycle.closure(),
203 saved
204 .closure_accounting
205 .ok_or(LiveFrontierRestoreError::MissingClosureAccounting)?,
206 )
207 .map_err(LiveFrontierRestoreError::AccountingCodec)?;
208 let charges = Self::decode_retained_charge_source(
209 saved
210 .retained_charges
211 .ok_or(LiveFrontierRestoreError::MissingRetainedCharges)?,
212 )
213 .map_err(LiveFrontierRestoreError::RetainedChargeCodec)?;
214 let episode = match lifecycle.closure() {
215 crate::lifecycle::ClosureState::Clear => {
216 if saved.cursor_episode.is_some() {
217 return Err(LiveFrontierRestoreError::UnexpectedCursorEpisode);
218 }
219 None
220 }
221 crate::lifecycle::ClosureState::Owed { debt, .. } => {
222 let bytes = saved
223 .cursor_episode
224 .ok_or(LiveFrontierRestoreError::MissingCursorEpisode)?;
225 Some(
226 crate::lifecycle::NonzeroDebtCursorEpisode::decode_checkpoint(
227 bytes,
228 lifecycle.frontiers(),
229 debt,
230 saved.observer_progress,
231 )
232 .map_err(LiveFrontierRestoreError::CursorEpisode)?,
233 )
234 }
235 };
236 Self::restore_checkpoint_parts(parts, accounting, charges, episode, saved)
237 }
238}
239
240#[derive(Clone, Copy, Debug)]
242pub struct FrontierCheckpointSource<'a> {
243 pub closure_accounting: Option<&'a [u8]>,
245 pub retained_charges: Option<&'a [u8]>,
247 pub cursor_episode: Option<&'a [u8]>,
249 pub observer_progress: u64,
251 pub retained_record_limit: u64,
253 pub marker_max: ResourceVector,
255}