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, ObservationReservationError};
15
16use crate::model::{
17 artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash},
18 bounded_sequence,
19};
20use history::{AttemptEventRecord, Projection};
21use serde::{Deserialize, Deserializer, Serialize};
22use thiserror::Error;
23
24pub const MAX_ATTEMPT_EVENTS: usize = 2048;
26pub const MAX_ATTEMPT_JOURNAL_BYTES: u64 = 1024 * 1024;
28
29#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
31#[serde(rename_all = "snake_case")]
32pub enum MutationOutcomeRecord {
33 Applied,
35 NotApplied,
37}
38
39#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
41#[serde(rename_all = "snake_case")]
42pub enum ObservationOutcomeRecord {
43 Applied,
45 NotApplied,
47 Uncertain,
49}
50
51#[derive(Clone, Debug)]
53pub struct MutationReceiptRequest {
54 pub attempt: u32,
56 pub request: String,
58 pub outcome: MutationOutcomeRecord,
60 pub evidence: String,
62}
63
64#[derive(Clone, Debug)]
70pub struct ObservationReceiptRequest {
71 pub attempt: u32,
73 pub request: String,
75 pub outcome: ObservationOutcomeRecord,
77 pub evidence: String,
79}
80
81#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
83pub struct AttemptJournalView {
84 pub authority: ArtifactChecksumRecord,
86 pub mutations_used: u32,
88 pub observations_used: u32,
90 pub mutations_remaining: u32,
92 pub observations_remaining: u32,
94 pub pending_mutation: Option<u32>,
96 pub pending_observation: Option<u32>,
98 pub applied: bool,
100 pub history_len: usize,
102}
103
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum AppliedReceiptEvidence<'a> {
110 Mutation {
112 attempt: u32,
114 evidence: &'a str,
116 },
117 Observation {
119 attempt: u32,
121 request: &'a str,
123 evidence: &'a str,
125 },
126}
127
128#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
130#[serde(try_from = "JournalFields")]
131pub struct AttemptJournalRecord {
132 version: u16,
133 authority: AttemptAuthorityRecord,
134 events: Vec<AttemptEventRecord>,
135 #[serde(skip)]
136 projection: Projection,
137}
138
139#[derive(Deserialize)]
140#[serde(deny_unknown_fields)]
141struct JournalFields {
142 version: u16,
143 authority: AttemptAuthorityRecord,
144 #[serde(deserialize_with = "bounded_events")]
145 events: Vec<AttemptEventRecord>,
146}
147
148impl TryFrom<JournalFields> for AttemptJournalRecord {
149 type Error = AttemptJournalRecordError;
150 fn try_from(mut fields: JournalFields) -> Result<Self, Self::Error> {
151 if fields.version != 1 {
152 return Err(AttemptJournalRecordError::UnsupportedVersion(
153 fields.version,
154 ));
155 }
156 let mut projection = Projection::empty();
157 for event in &mut fields.events {
158 event.normalize()?;
159 projection.apply(event, fields.authority.budget())?;
160 }
161 Ok(Self {
162 version: 1,
163 authority: fields.authority,
164 events: fields.events,
165 projection,
166 })
167 }
168}
169
170impl AttemptJournalRecord {
171 #[must_use]
173 pub const fn new(authority: AttemptAuthorityRecord) -> Self {
174 Self {
175 version: 1,
176 authority,
177 events: Vec::new(),
178 projection: Projection::empty(),
179 }
180 }
181 #[must_use]
183 pub const fn authority(&self) -> &AttemptAuthorityRecord {
184 &self.authority
185 }
186 pub(crate) fn validate_pending_mutation(
189 &self,
190 authority: &AttemptAuthorityRecord,
191 mutation_attempt: u32,
192 ) -> Result<(), MutationReservationError> {
193 if self.authority() != authority {
194 return Err(MutationReservationError::AuthorityMismatch);
195 }
196 if self.pending_mutation() != Some(mutation_attempt) {
197 return Err(MutationReservationError::MutationMismatch);
198 }
199 if self.pending_observation().is_some() {
200 return Err(MutationReservationError::ObservationPending);
201 }
202 Ok(())
203 }
204 pub(crate) const fn pending_mutation(&self) -> Option<u32> {
207 self.projection.pending_mutation()
208 }
209 pub(crate) const fn pending_observation(&self) -> Option<u32> {
210 match self.projection.pending_observation() {
211 Some(pending) => Some(pending.attempt),
212 None => None,
213 }
214 }
215 #[must_use]
220 pub fn pending_observation_request(&self) -> Option<&str> {
221 self.projection
222 .pending_observation()
223 .map(|pending| pending.request.as_str())
224 }
225 #[must_use]
229 pub fn applied_receipt(&self) -> Option<AppliedReceiptEvidence<'_>> {
230 match self.events.last()? {
231 AttemptEventRecord::MutationResolved {
232 mutation,
233 outcome: MutationOutcomeRecord::Applied,
234 evidence,
235 } => Some(AppliedReceiptEvidence::Mutation {
236 attempt: *mutation,
237 evidence,
238 }),
239 AttemptEventRecord::ObservationRecorded {
240 observation,
241 request,
242 outcome: ObservationOutcomeRecord::Applied,
243 evidence,
244 } => Some(AppliedReceiptEvidence::Observation {
245 attempt: *observation,
246 request,
247 evidence,
248 }),
249 _ => None,
250 }
251 }
252 #[must_use]
257 pub fn view(&self) -> AttemptJournalView {
258 AttemptJournalView {
259 authority: self.authority.digest(),
260 mutations_used: self.projection.mutations_used,
261 observations_used: self.projection.observations_used,
262 mutations_remaining: self.authority.budget().mutations()
263 - self.projection.mutations_used,
264 observations_remaining: self.authority.budget().observations()
265 - self.projection.observations_used,
266 pending_mutation: self.projection.pending_mutation(),
267 pending_observation: self
268 .projection
269 .pending_observation()
270 .map(|pending| pending.attempt),
271 applied: self.projection.applied(),
272 history_len: self.events.len(),
273 }
274 }
275 #[must_use]
281 pub fn digest(&self) -> ArtifactChecksumRecord {
282 let mut bytes = EvidenceDigest::new(b"ic-backup/attempt-journal-history/v1\0");
283 bytes.checksum(&self.authority.digest());
284 bytes.u64_be(self.events.len() as u64);
285 for event in &self.events {
286 event.append_digest_bytes(&mut bytes);
287 }
288 bytes.finish()
289 }
290 pub(crate) fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalRecordError> {
291 let attempt = self.projection.next_attempt;
292 self.append(AttemptEventRecord::MutationReserved { attempt })?;
293 Ok(attempt)
294 }
295 pub(crate) fn reserve_observation(
296 &mut self,
297 mutation: u32,
298 request: &str,
299 ) -> Result<u32, AttemptJournalRecordError> {
300 let attempt = self.projection.next_attempt;
301 self.append(AttemptEventRecord::ObservationReserved {
302 attempt,
303 mutation,
304 request: request.to_owned(),
305 })?;
306 Ok(attempt)
307 }
308 pub(crate) fn record_mutation(
309 &mut self,
310 receipt: MutationReceiptRequest,
311 ) -> Result<(), AttemptJournalRecordError> {
312 if canonical_hash(&receipt.request)? != self.authority.binding().request() {
313 return Err(AttemptJournalRecordError::RequestMismatch);
314 }
315 self.append(AttemptEventRecord::MutationResolved {
316 mutation: receipt.attempt,
317 outcome: receipt.outcome,
318 evidence: receipt.evidence,
319 })
320 }
321 pub(crate) fn record_observation(
322 &mut self,
323 receipt: ObservationReceiptRequest,
324 ) -> Result<(), AttemptJournalRecordError> {
325 self.append(AttemptEventRecord::ObservationRecorded {
326 observation: receipt.attempt,
327 request: receipt.request,
328 outcome: receipt.outcome,
329 evidence: receipt.evidence,
330 })
331 }
332 fn append(&mut self, mut event: AttemptEventRecord) -> Result<(), AttemptJournalRecordError> {
333 if self.events.len() == MAX_ATTEMPT_EVENTS {
334 return Err(AttemptJournalRecordError::HistoryTooLarge);
335 }
336 event.normalize()?;
337 let mut projection = self.projection.clone();
338 projection.apply(&event, self.authority.budget())?;
339 self.events.push(event);
340 self.projection = projection;
341 Ok(())
342 }
343}
344
345fn bounded_events<'de, D: Deserializer<'de>>(
346 deserializer: D,
347) -> Result<Vec<AttemptEventRecord>, D::Error> {
348 bounded_sequence::deserialize::<_, _, MAX_ATTEMPT_EVENTS>(
349 deserializer,
350 "a bounded chronological attempt event list",
351 AttemptJournalRecordError::HistoryTooLarge,
352 )
353}
354
355#[derive(Debug, Error)]
357pub enum AttemptJournalRecordError {
358 #[error("unsupported attempt journal version {0}")]
360 UnsupportedVersion(u16),
361 #[error("attempt allowance exceeds {MAX_OPERATION_ATTEMPTS}")]
363 BudgetTooLarge,
364 #[error("attempt history exceeds {MAX_ATTEMPT_EVENTS} events")]
366 HistoryTooLarge,
367 #[error("invalid attempt operation principal")]
369 InvalidPrincipal,
370 #[error(transparent)]
372 Checksum(#[from] ChecksumError),
373 #[error("mutation attempt {attempt} remains unresolved")]
375 MutationPending {
376 attempt: u32,
378 },
379 #[error("observation attempt {attempt} remains unresolved")]
381 ObservationPending {
382 attempt: u32,
384 },
385 #[error("no pending mutation attempt")]
387 NoPendingMutation,
388 #[error("no pending observation attempt")]
390 NoPendingObservation,
391 #[error("attempt identity mismatch: expected {expected}, actual {actual}")]
393 AttemptMismatch {
394 expected: u32,
396 actual: u32,
398 },
399 #[error("attempt request digest mismatch")]
401 RequestMismatch,
402 #[error("mutation attempt allowance exhausted")]
404 MutationBudgetExhausted,
405 #[error("observation attempt allowance exhausted")]
407 ObservationBudgetExhausted,
408 #[error("operation application already retained")]
410 AlreadyApplied,
411}
412
413#[cfg(test)]
414mod tests;