1use crate::model::evidence_digest::EvidenceDigest;
4mod authority;
5mod history;
6pub use authority::{
7 AttemptAuthorityRecord, AttemptBudgetRecord, MAX_OPERATION_ATTEMPTS, OperationBindingRecord,
8 OperationBindingRequest,
9};
10
11use crate::model::{
12 artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash},
13 bounded_sequence,
14};
15use history::{AttemptEventRecord, Projection};
16use serde::{Deserialize, Deserializer, Serialize};
17use thiserror::Error;
18
19pub const MAX_ATTEMPT_EVENTS: usize = 2048;
21pub const MAX_ATTEMPT_JOURNAL_BYTES: u64 = 1024 * 1024;
23
24#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
26#[serde(rename_all = "snake_case")]
27pub enum MutationOutcomeRecord {
28 Applied,
30 NotApplied,
32}
33
34#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
36#[serde(rename_all = "snake_case")]
37pub enum ObservationOutcomeRecord {
38 Applied,
40 NotApplied,
42 Uncertain,
44}
45
46#[derive(Clone, Debug)]
48pub struct MutationReceiptRequest {
49 pub attempt: u32,
51 pub request: String,
53 pub outcome: MutationOutcomeRecord,
55 pub evidence: String,
57}
58
59#[derive(Clone, Debug)]
65pub struct ObservationReceiptRequest {
66 pub attempt: u32,
68 pub request: String,
70 pub outcome: ObservationOutcomeRecord,
72 pub evidence: String,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
78pub struct AttemptJournalView {
79 pub authority: ArtifactChecksumRecord,
81 pub mutations_used: u32,
83 pub observations_used: u32,
85 pub mutations_remaining: u32,
87 pub observations_remaining: u32,
89 pub pending_mutation: Option<u32>,
91 pub pending_observation: Option<u32>,
93 pub applied: bool,
95 pub history_len: usize,
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub enum AppliedReceiptEvidence<'a> {
105 Mutation {
107 attempt: u32,
109 evidence: &'a str,
111 },
112 Observation {
114 attempt: u32,
116 request: &'a str,
118 evidence: &'a str,
120 },
121}
122
123#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
125#[serde(try_from = "JournalFields")]
126pub struct AttemptJournalRecord {
127 version: u16,
128 authority: AttemptAuthorityRecord,
129 events: Vec<AttemptEventRecord>,
130 #[serde(skip)]
131 projection: Projection,
132}
133
134#[derive(Deserialize)]
135#[serde(deny_unknown_fields)]
136struct JournalFields {
137 version: u16,
138 authority: AttemptAuthorityRecord,
139 #[serde(deserialize_with = "bounded_events")]
140 events: Vec<AttemptEventRecord>,
141}
142
143impl TryFrom<JournalFields> for AttemptJournalRecord {
144 type Error = AttemptJournalRecordError;
145 fn try_from(mut fields: JournalFields) -> Result<Self, Self::Error> {
146 if fields.version != 1 {
147 return Err(AttemptJournalRecordError::UnsupportedVersion(
148 fields.version,
149 ));
150 }
151 let mut projection = Projection::empty();
152 for event in &mut fields.events {
153 event.normalize()?;
154 projection.apply(event, fields.authority.budget())?;
155 }
156 Ok(Self {
157 version: 1,
158 authority: fields.authority,
159 events: fields.events,
160 projection,
161 })
162 }
163}
164
165impl AttemptJournalRecord {
166 #[must_use]
168 pub const fn new(authority: AttemptAuthorityRecord) -> Self {
169 Self {
170 version: 1,
171 authority,
172 events: Vec::new(),
173 projection: Projection::empty(),
174 }
175 }
176 #[must_use]
178 pub const fn authority(&self) -> &AttemptAuthorityRecord {
179 &self.authority
180 }
181 pub(crate) fn validate_pending_mutation<E>(
184 &self,
185 authority: &AttemptAuthorityRecord,
186 mutation_attempt: u32,
187 authority_mismatch: E,
188 mutation_mismatch: E,
189 observation_pending: E,
190 ) -> Result<(), E> {
191 if self.authority() != authority {
192 return Err(authority_mismatch);
193 }
194 if self.pending_mutation() != Some(mutation_attempt) {
195 return Err(mutation_mismatch);
196 }
197 if self.pending_observation().is_some() {
198 return Err(observation_pending);
199 }
200 Ok(())
201 }
202 pub(crate) const fn pending_mutation(&self) -> Option<u32> {
205 self.projection.pending_mutation()
206 }
207 pub(crate) const fn pending_observation(&self) -> Option<u32> {
208 match self.projection.pending_observation() {
209 Some(pending) => Some(pending.attempt),
210 None => None,
211 }
212 }
213 #[must_use]
218 pub fn pending_observation_request(&self) -> Option<&str> {
219 self.projection
220 .pending_observation()
221 .map(|pending| pending.request.as_str())
222 }
223 #[must_use]
227 pub fn applied_receipt(&self) -> Option<AppliedReceiptEvidence<'_>> {
228 match self.events.last()? {
229 AttemptEventRecord::MutationResolved {
230 mutation,
231 outcome: MutationOutcomeRecord::Applied,
232 evidence,
233 } => Some(AppliedReceiptEvidence::Mutation {
234 attempt: *mutation,
235 evidence,
236 }),
237 AttemptEventRecord::ObservationRecorded {
238 observation,
239 request,
240 outcome: ObservationOutcomeRecord::Applied,
241 evidence,
242 } => Some(AppliedReceiptEvidence::Observation {
243 attempt: *observation,
244 request,
245 evidence,
246 }),
247 _ => None,
248 }
249 }
250 #[must_use]
255 pub fn view(&self) -> AttemptJournalView {
256 AttemptJournalView {
257 authority: self.authority.digest(),
258 mutations_used: self.projection.mutations_used,
259 observations_used: self.projection.observations_used,
260 mutations_remaining: self.authority.budget().mutations()
261 - self.projection.mutations_used,
262 observations_remaining: self.authority.budget().observations()
263 - self.projection.observations_used,
264 pending_mutation: self.projection.pending_mutation(),
265 pending_observation: self
266 .projection
267 .pending_observation()
268 .map(|pending| pending.attempt),
269 applied: self.projection.applied(),
270 history_len: self.events.len(),
271 }
272 }
273 #[must_use]
279 pub fn digest(&self) -> ArtifactChecksumRecord {
280 let mut bytes = EvidenceDigest::new(b"ic-backup/attempt-journal-history/v1\0");
281 bytes.checksum(&self.authority.digest());
282 bytes.u64_be(self.events.len() as u64);
283 for event in &self.events {
284 event.append_digest_bytes(&mut bytes);
285 }
286 bytes.finish()
287 }
288 pub(crate) fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalRecordError> {
289 let attempt = self.projection.next_attempt;
290 self.append(AttemptEventRecord::MutationReserved { attempt })?;
291 Ok(attempt)
292 }
293 pub(crate) fn reserve_observation(
294 &mut self,
295 mutation: u32,
296 request: &str,
297 ) -> Result<u32, AttemptJournalRecordError> {
298 let attempt = self.projection.next_attempt;
299 self.append(AttemptEventRecord::ObservationReserved {
300 attempt,
301 mutation,
302 request: request.to_owned(),
303 })?;
304 Ok(attempt)
305 }
306 pub(crate) fn record_mutation(
307 &mut self,
308 receipt: MutationReceiptRequest,
309 ) -> Result<(), AttemptJournalRecordError> {
310 if canonical_hash(&receipt.request)? != self.authority.binding().request() {
311 return Err(AttemptJournalRecordError::RequestMismatch);
312 }
313 self.append(AttemptEventRecord::MutationResolved {
314 mutation: receipt.attempt,
315 outcome: receipt.outcome,
316 evidence: receipt.evidence,
317 })
318 }
319 pub(crate) fn record_observation(
320 &mut self,
321 receipt: ObservationReceiptRequest,
322 ) -> Result<(), AttemptJournalRecordError> {
323 self.append(AttemptEventRecord::ObservationRecorded {
324 observation: receipt.attempt,
325 request: receipt.request,
326 outcome: receipt.outcome,
327 evidence: receipt.evidence,
328 })
329 }
330 fn append(&mut self, mut event: AttemptEventRecord) -> Result<(), AttemptJournalRecordError> {
331 if self.events.len() == MAX_ATTEMPT_EVENTS {
332 return Err(AttemptJournalRecordError::HistoryTooLarge);
333 }
334 event.normalize()?;
335 let mut projection = self.projection.clone();
336 projection.apply(&event, self.authority.budget())?;
337 self.events.push(event);
338 self.projection = projection;
339 Ok(())
340 }
341}
342
343fn bounded_events<'de, D: Deserializer<'de>>(
344 deserializer: D,
345) -> Result<Vec<AttemptEventRecord>, D::Error> {
346 bounded_sequence::deserialize::<_, _, MAX_ATTEMPT_EVENTS>(
347 deserializer,
348 "a bounded chronological attempt event list",
349 AttemptJournalRecordError::HistoryTooLarge,
350 )
351}
352
353#[derive(Debug, Error)]
355pub enum AttemptJournalRecordError {
356 #[error("unsupported attempt journal version {0}")]
358 UnsupportedVersion(u16),
359 #[error("attempt allowance exceeds {MAX_OPERATION_ATTEMPTS}")]
361 BudgetTooLarge,
362 #[error("attempt history exceeds {MAX_ATTEMPT_EVENTS} events")]
364 HistoryTooLarge,
365 #[error("invalid attempt operation principal")]
367 InvalidPrincipal,
368 #[error(transparent)]
370 Checksum(#[from] ChecksumError),
371 #[error("mutation attempt {attempt} remains unresolved")]
373 MutationPending {
374 attempt: u32,
376 },
377 #[error("observation attempt {attempt} remains unresolved")]
379 ObservationPending {
380 attempt: u32,
382 },
383 #[error("no pending mutation attempt")]
385 NoPendingMutation,
386 #[error("no pending observation attempt")]
388 NoPendingObservation,
389 #[error("attempt identity mismatch: expected {expected}, actual {actual}")]
391 AttemptMismatch {
392 expected: u32,
394 actual: u32,
396 },
397 #[error("attempt request digest mismatch")]
399 RequestMismatch,
400 #[error("mutation attempt allowance exhausted")]
402 MutationBudgetExhausted,
403 #[error("observation attempt allowance exhausted")]
405 ObservationBudgetExhausted,
406 #[error("operation application already retained")]
408 AlreadyApplied,
409}
410
411#[cfg(test)]
412mod tests;