1use crate::model::evidence_digest::EvidenceDigest;
4mod authority;
5mod history;
6mod mutation;
7pub(crate) use mutation::MutationReservation;
8pub use mutation::MutationReservationError;
9mod observation;
10pub use authority::{
11 AttemptAuthorityRecord, AttemptBudgetRecord, MAX_OPERATION_ATTEMPTS, OperationBindingRecord,
12 OperationBindingRequest,
13};
14pub(crate) use observation::ObservationReservation;
15pub use observation::ObservationReservationError;
16
17use crate::model::{
18 artifacts::{ArtifactChecksumRecord, ChecksumError},
19 bounded_sequence,
20};
21use history::{AttemptEventRecord, Projection};
22use serde::{Deserialize, Deserializer, Serialize};
23use thiserror::Error;
24
25pub const MAX_ATTEMPT_EVENTS: usize = 2048;
27pub const MAX_ATTEMPT_JOURNAL_BYTES: u64 = 1024 * 1024;
29
30#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(rename_all = "snake_case")]
33pub enum MutationOutcomeRecord {
34 Applied,
36 NotApplied,
38}
39
40#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
42#[serde(rename_all = "snake_case")]
43pub enum ObservationOutcomeRecord {
44 Applied,
46 NotApplied,
48 Uncertain,
50}
51
52#[derive(Clone, Debug)]
54pub struct MutationReceiptRequest {
55 pub attempt: u32,
57 pub request: ArtifactChecksumRecord,
59 pub outcome: MutationOutcomeRecord,
61 pub evidence: ArtifactChecksumRecord,
63}
64
65#[derive(Clone, Debug)]
71pub struct ObservationReceiptRequest {
72 pub attempt: u32,
74 pub request: ArtifactChecksumRecord,
76 pub outcome: ObservationOutcomeRecord,
78 pub evidence: ArtifactChecksumRecord,
80}
81
82#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
84pub struct AttemptJournalView {
85 pub authority: ArtifactChecksumRecord,
87 pub mutations_used: u32,
89 pub observations_used: u32,
91 pub mutations_remaining: u32,
93 pub observations_remaining: u32,
95 pub pending_mutation: Option<u32>,
97 pub pending_observation: Option<u32>,
99 pub applied: bool,
101 pub history_len: usize,
103}
104
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum AppliedReceiptEvidence<'a> {
111 Mutation {
113 attempt: u32,
115 evidence: &'a str,
117 },
118 Observation {
120 attempt: u32,
122 request: &'a str,
124 evidence: &'a str,
126 },
127}
128
129#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
131#[serde(try_from = "JournalFields")]
132pub struct AttemptJournalRecord {
133 version: u16,
134 authority: AttemptAuthorityRecord,
135 events: Vec<AttemptEventRecord>,
136 #[serde(skip)]
137 projection: Projection,
138}
139
140#[derive(Deserialize)]
141#[serde(deny_unknown_fields)]
142struct JournalFields {
143 version: u16,
144 authority: AttemptAuthorityRecord,
145 #[serde(deserialize_with = "bounded_events")]
146 events: Vec<AttemptEventRecord>,
147}
148
149impl TryFrom<JournalFields> for AttemptJournalRecord {
150 type Error = AttemptJournalRecordError;
151 fn try_from(mut fields: JournalFields) -> Result<Self, Self::Error> {
152 if fields.version != 1 {
153 return Err(AttemptJournalRecordError::UnsupportedVersion(
154 fields.version,
155 ));
156 }
157 let mut projection = Projection::empty();
158 for event in &mut fields.events {
159 event.normalize()?;
160 projection.apply(event, fields.authority.budget())?;
161 }
162 Ok(Self {
163 version: 1,
164 authority: fields.authority,
165 events: fields.events,
166 projection,
167 })
168 }
169}
170
171impl AttemptJournalRecord {
172 #[must_use]
174 pub const fn new(authority: AttemptAuthorityRecord) -> Self {
175 Self {
176 version: 1,
177 authority,
178 events: Vec::new(),
179 projection: Projection::empty(),
180 }
181 }
182 #[must_use]
184 pub const fn authority(&self) -> &AttemptAuthorityRecord {
185 &self.authority
186 }
187 pub(crate) fn validate_pending_mutation(
190 &self,
191 authority: &AttemptAuthorityRecord,
192 mutation_attempt: u32,
193 ) -> Result<(), MutationReservationError> {
194 if self.authority() != authority {
195 return Err(MutationReservationError::AuthorityMismatch);
196 }
197 if self.pending_mutation() != Some(mutation_attempt) {
198 return Err(MutationReservationError::MutationMismatch);
199 }
200 if self.pending_observation().is_some() {
201 return Err(MutationReservationError::ObservationPending);
202 }
203 Ok(())
204 }
205 pub(crate) const fn pending_mutation(&self) -> Option<u32> {
208 self.projection.pending_mutation()
209 }
210 pub(crate) const fn pending_observation(&self) -> Option<u32> {
211 match self.projection.pending_observation() {
212 Some(pending) => Some(pending.attempt),
213 None => None,
214 }
215 }
216 #[must_use]
221 pub fn pending_observation_request(&self) -> Option<&str> {
222 self.projection
223 .pending_observation()
224 .map(|pending| pending.request.as_str())
225 }
226 #[must_use]
230 pub fn applied_receipt(&self) -> Option<AppliedReceiptEvidence<'_>> {
231 match self.events.last()? {
232 AttemptEventRecord::MutationResolved {
233 mutation,
234 outcome: MutationOutcomeRecord::Applied,
235 evidence,
236 } => Some(AppliedReceiptEvidence::Mutation {
237 attempt: *mutation,
238 evidence,
239 }),
240 AttemptEventRecord::ObservationRecorded {
241 observation,
242 request,
243 outcome: ObservationOutcomeRecord::Applied,
244 evidence,
245 } => Some(AppliedReceiptEvidence::Observation {
246 attempt: *observation,
247 request,
248 evidence,
249 }),
250 _ => None,
251 }
252 }
253 pub(crate) const fn mutations_used(&self) -> u32 {
254 self.projection.mutations_used
255 }
256
257 #[must_use]
262 pub fn view(&self) -> AttemptJournalView {
263 AttemptJournalView {
264 authority: self.authority.digest(),
265 mutations_used: self.projection.mutations_used,
266 observations_used: self.projection.observations_used,
267 mutations_remaining: self.authority.budget().mutations()
268 - self.projection.mutations_used,
269 observations_remaining: self.authority.budget().observations()
270 - self.projection.observations_used,
271 pending_mutation: self.projection.pending_mutation(),
272 pending_observation: self
273 .projection
274 .pending_observation()
275 .map(|pending| pending.attempt),
276 applied: self.projection.applied(),
277 history_len: self.events.len(),
278 }
279 }
280 #[must_use]
286 pub fn digest(&self) -> ArtifactChecksumRecord {
287 let mut bytes = EvidenceDigest::new(b"ic-backup/attempt-journal-history/v1\0");
288 bytes.checksum(&self.authority.digest());
289 bytes.u64_be(self.events.len() as u64);
290 for event in &self.events {
291 event.append_digest_bytes(&mut bytes);
292 }
293 bytes.finish()
294 }
295 pub(crate) fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalRecordError> {
296 let attempt = self.projection.next_attempt;
297 self.append(AttemptEventRecord::MutationReserved { attempt })?;
298 Ok(attempt)
299 }
300 pub(crate) fn reserve_observation(
301 &mut self,
302 mutation: u32,
303 request: &str,
304 ) -> Result<u32, AttemptJournalRecordError> {
305 let attempt = self.projection.next_attempt;
306 self.append(AttemptEventRecord::ObservationReserved {
307 attempt,
308 mutation,
309 request: request.to_owned(),
310 })?;
311 Ok(attempt)
312 }
313 pub(crate) fn record_mutation(
314 &mut self,
315 receipt: MutationReceiptRequest,
316 ) -> Result<(), AttemptJournalRecordError> {
317 if receipt.request.hash() != self.authority.binding().request() {
318 return Err(AttemptJournalRecordError::RequestMismatch);
319 }
320 self.append(AttemptEventRecord::MutationResolved {
321 mutation: receipt.attempt,
322 outcome: receipt.outcome,
323 evidence: receipt.evidence.into_hash(),
324 })
325 }
326 pub(crate) fn record_observation(
327 &mut self,
328 receipt: ObservationReceiptRequest,
329 ) -> Result<(), AttemptJournalRecordError> {
330 self.append(AttemptEventRecord::ObservationRecorded {
331 observation: receipt.attempt,
332 request: receipt.request.into_hash(),
333 outcome: receipt.outcome,
334 evidence: receipt.evidence.into_hash(),
335 })
336 }
337 fn append(&mut self, mut event: AttemptEventRecord) -> Result<(), AttemptJournalRecordError> {
338 if self.events.len() == MAX_ATTEMPT_EVENTS {
339 return Err(AttemptJournalRecordError::HistoryTooLarge);
340 }
341 event.normalize()?;
342 let mut projection = self.projection.clone();
343 projection.apply(&event, self.authority.budget())?;
344 self.events.push(event);
345 self.projection = projection;
346 Ok(())
347 }
348}
349
350fn bounded_events<'de, D: Deserializer<'de>>(
351 deserializer: D,
352) -> Result<Vec<AttemptEventRecord>, D::Error> {
353 bounded_sequence::deserialize::<_, _, MAX_ATTEMPT_EVENTS>(
354 deserializer,
355 "a bounded chronological attempt event list",
356 AttemptJournalRecordError::HistoryTooLarge,
357 )
358}
359
360#[derive(Debug, Error)]
362pub enum AttemptJournalRecordError {
363 #[error("unsupported attempt journal version {0}")]
365 UnsupportedVersion(u16),
366 #[error("attempt allowance exceeds {MAX_OPERATION_ATTEMPTS}")]
368 BudgetTooLarge,
369 #[error("attempt history exceeds {MAX_ATTEMPT_EVENTS} events")]
371 HistoryTooLarge,
372 #[error("invalid attempt operation principal")]
374 InvalidPrincipal,
375 #[error(transparent)]
377 Checksum(#[from] ChecksumError),
378 #[error("mutation attempt {attempt} remains unresolved")]
380 MutationPending {
381 attempt: u32,
383 },
384 #[error("observation attempt {attempt} remains unresolved")]
386 ObservationPending {
387 attempt: u32,
389 },
390 #[error("no pending mutation attempt")]
392 NoPendingMutation,
393 #[error("no pending observation attempt")]
395 NoPendingObservation,
396 #[error("attempt identity mismatch: expected {expected}, actual {actual}")]
398 AttemptMismatch {
399 expected: u32,
401 actual: u32,
403 },
404 #[error("attempt request digest mismatch")]
406 RequestMismatch,
407 #[error("mutation attempt allowance exhausted")]
409 MutationBudgetExhausted,
410 #[error("observation attempt allowance exhausted")]
412 ObservationBudgetExhausted,
413 #[error("operation application already retained")]
415 AlreadyApplied,
416}
417
418#[cfg(test)]
419mod tests;