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};
10use crate::wire::CredentialDigest;
11
12use super::LiveFrontierOwner;
13
14pub type CheckpointParticipant = RestoredParticipantLifecycle<
16 CredentialDigest,
17 CredentialDigest,
18 CredentialDigest,
19 CredentialDigest,
20>;
21
22#[derive(Debug)]
24#[non_exhaustive]
25pub struct RestoredLiveFrontierOwner {
26 pub conversation: ParticipantConversation,
28 pub participants: Vec<CheckpointParticipant>,
30 pub owner: LiveFrontierOwner,
32 pub cursor_episode: Option<crate::lifecycle::NonzeroDebtCursorEpisode>,
34 pub(in crate::lifecycle) observer_progress: u64,
35}
36
37#[derive(Debug, PartialEq, Eq)]
39pub enum LiveFrontierRestoreError {
40 MissingLifecycle,
42 MissingClosureAccounting,
44 MissingRetainedCharges,
46 MissingCursorEpisode,
48 UnexpectedCursorEpisode,
50 AccountingCodec(crate::lifecycle::FrontierAccountingDecodeError),
52 RetainedChargeCodec(crate::lifecycle::CapsuleDecodeError),
54 CursorEpisode(crate::lifecycle::StorageRestoreError),
56 CursorEpisodeEncoding(crate::lifecycle::CursorFactEncodeError),
58 ForeignConversation,
60 ClosureMismatch,
62 RetainedRecordLimit,
64 Accounting(OrdinaryProjectionError),
66}
67
68impl LiveFrontierOwner {
69 fn restore_checkpoint_parts(
78 parts: RestoredLifecycleParts,
79 accounting: ClosureAccounting,
80 charges: Vec<RetainedRecordCharge>,
81 cursor_episode: Option<crate::lifecycle::NonzeroDebtCursorEpisode>,
82 saved: FrontierCheckpointSource<'_>,
83 ) -> Result<RestoredLiveFrontierOwner, LiveFrontierRestoreError> {
84 let lifecycle = parts
85 .lifecycle
86 .ok_or(LiveFrontierRestoreError::MissingLifecycle)?;
87 if lifecycle.frontiers().conversation_id() != parts.conversation.conversation_id() {
88 return Err(LiveFrontierRestoreError::ForeignConversation);
89 }
90 if lifecycle.closure() != accounting.state() {
91 return Err(LiveFrontierRestoreError::ClosureMismatch);
92 }
93 if lifecycle.frontiers().retained_records().len() as u128
98 > u128::from(saved.retained_record_limit)
99 .saturating_add(u128::from(lifecycle.frontiers().identity_slot_limit()))
100 {
101 return Err(LiveFrontierRestoreError::RetainedRecordLimit);
102 }
103 validate_saved_accounting(
104 lifecycle.frontiers(),
105 &accounting,
106 &charges,
107 saved.marker_max,
108 )
109 .map_err(LiveFrontierRestoreError::Accounting)?;
110 let (participants, frontiers, _) = lifecycle.into_parts();
111 Ok(RestoredLiveFrontierOwner {
112 conversation: parts.conversation,
113 participants,
114 cursor_episode,
115 observer_progress: saved.observer_progress,
116 owner: Self {
117 frontiers,
118 closure_accounting: accounting,
119 retained_charges: charges,
120 retained_record_limit: saved.retained_record_limit,
121 },
122 })
123 }
124}
125
126fn validate_saved_accounting(
127 frontiers: &crate::lifecycle::ClaimFrontiers,
128 accounting: &ClosureAccounting,
129 charges: &[RetainedRecordCharge],
130 marker_max: ResourceVector,
131) -> Result<(), OrdinaryProjectionError> {
132 let records = frontiers.retained_records();
133 let high_end = u128::from(frontiers.sequence().ledger().high_watermark()) + 1;
134 if high_end.checked_sub(frontiers.retained_floor()) != Some(records.len() as u128)
135 || records.len() != charges.len()
136 {
137 return Err(OrdinaryProjectionError::RetainedSuffix);
138 }
139 let credited: Vec<_> = frontiers
147 .retained_marker_records()
148 .iter()
149 .map(|record| record.delivery_seq)
150 .collect();
151 let mut retained = WideResourceVector::default();
152 for (index, (record, charge)) in records.iter().zip(charges).enumerate() {
153 if u128::from(record.delivery_seq) != frontiers.retained_floor() + index as u128
154 || charge.delivery_seq() != record.delivery_seq
155 || charge.admission_order() != record.admission_order
156 {
157 return Err(OrdinaryProjectionError::RetainedChargeKey {
158 index: u64::try_from(index).map_err(|_| OrdinaryProjectionError::RetainedSuffix)?,
159 });
160 }
161 let resource = charge.encoded_charge();
162 if resource.entries != 1 {
163 return Err(OrdinaryProjectionError::RetainedEntryCharge {
164 delivery_seq: charge.delivery_seq(),
165 entries: resource.entries,
166 });
167 }
168 let resource = if credited.contains(&record.delivery_seq) {
169 marker_max
170 } else {
171 resource
172 };
173 retained =
174 super::super::ordinary_record_projection::checked_add_resource(retained, resource)?;
175 }
176 let pending = frontiers.pending_marker_candidate_count();
177 let retained_credits = u64::try_from(frontiers.retained_marker_records().len())
178 .map_err(|_| OrdinaryProjectionError::MarkerCredit)?;
179 let credits = retained_credits
180 .checked_add(pending)
181 .ok_or(OrdinaryProjectionError::MarkerCredit)?;
182 if credits != accounting.marker_capacity_credits() {
183 return Err(OrdinaryProjectionError::MarkerCreditAccounting {
184 derived: credits,
185 stored: accounting.marker_capacity_credits(),
186 });
187 }
188 let anchors = frontiers
189 .unaccepted_marker_anchor_count()
190 .checked_add(pending)
191 .ok_or(OrdinaryProjectionError::MarkerAnchor)?;
192 if anchors != accounting.marker_anchors() {
193 return Err(OrdinaryProjectionError::MarkerAnchorAccounting {
194 derived: anchors,
195 stored: accounting.marker_anchors(),
196 });
197 }
198 let baseline = super::super::ordinary_record_projection::retained_baseline_wide(
199 retained,
200 frontiers.identity_slot_limit(),
201 retained_credits,
202 marker_max,
203 )?;
204 if baseline != accounting.baseline() {
205 return Err(OrdinaryProjectionError::BaselineAccounting {
206 derived: baseline,
207 stored: accounting.baseline(),
208 });
209 }
210 Ok(())
211}
212
213impl LiveFrontierOwner {
214 pub fn restore_checkpoint(
221 parts: RestoredLifecycleParts,
222 saved: FrontierCheckpointSource<'_>,
223 ) -> Result<RestoredLiveFrontierOwner, LiveFrontierRestoreError> {
224 let lifecycle = parts
225 .lifecycle
226 .as_ref()
227 .ok_or(LiveFrontierRestoreError::MissingLifecycle)?;
228 let accounting = Self::decode_closure_accounting_source(
229 lifecycle.closure(),
230 saved
231 .closure_accounting
232 .ok_or(LiveFrontierRestoreError::MissingClosureAccounting)?,
233 )
234 .map_err(LiveFrontierRestoreError::AccountingCodec)?;
235 let charges = Self::decode_retained_charge_source(
236 saved
237 .retained_charges
238 .ok_or(LiveFrontierRestoreError::MissingRetainedCharges)?,
239 )
240 .map_err(LiveFrontierRestoreError::RetainedChargeCodec)?;
241 let episode = match lifecycle.closure() {
242 crate::lifecycle::ClosureState::Clear => {
243 if saved.cursor_episode.is_some() {
244 return Err(LiveFrontierRestoreError::UnexpectedCursorEpisode);
245 }
246 None
247 }
248 crate::lifecycle::ClosureState::Owed { debt, .. } => {
249 let bytes = saved
250 .cursor_episode
251 .ok_or(LiveFrontierRestoreError::MissingCursorEpisode)?;
252 Some(
253 crate::lifecycle::NonzeroDebtCursorEpisode::decode_checkpoint(
254 bytes,
255 lifecycle.frontiers(),
256 debt,
257 saved.observer_progress,
258 )
259 .map_err(LiveFrontierRestoreError::CursorEpisode)?,
260 )
261 }
262 };
263 Self::restore_checkpoint_parts(parts, accounting, charges, episode, saved)
264 }
265}
266
267#[derive(Clone, Copy, Debug)]
269pub struct FrontierCheckpointSource<'a> {
270 pub closure_accounting: Option<&'a [u8]>,
272 pub retained_charges: Option<&'a [u8]>,
274 pub cursor_episode: Option<&'a [u8]>,
276 pub observer_progress: u64,
278 pub retained_record_limit: u64,
280 pub marker_max: ResourceVector,
282}