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