ic_backup/model/execution_settlement/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord, attempt_journal::AttemptJournalRecord, bounded_sequence,
5 effect_graph::MAX_EFFECT_OPERATIONS,
6};
7use serde::{Deserialize, Deserializer, Serialize};
8use thiserror::Error;
9
10pub const MAX_EXECUTION_SETTLEMENT_BYTES: u64 = 2 * 1024 * 1024;
12
13#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
15#[serde(deny_unknown_fields)]
16pub struct ExecutionSettlementJournalRecord {
17 operation_sequence: u64,
18 history: ArtifactChecksumRecord,
19}
20impl ExecutionSettlementJournalRecord {
21 #[must_use]
23 pub fn from_journal(journal: &AttemptJournalRecord) -> Self {
24 Self {
25 operation_sequence: journal.authority().binding().operation_sequence(),
26 history: journal.digest(),
27 }
28 }
29 #[must_use]
31 pub const fn operation_sequence(&self) -> u64 {
32 self.operation_sequence
33 }
34 #[must_use]
36 pub const fn history(&self) -> &ArtifactChecksumRecord {
37 &self.history
38 }
39}
40
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
48#[serde(try_from = "SettlementFields")]
49pub struct ExecutionSettlementRecord {
50 version: u16,
51 plan_intent: ArtifactChecksumRecord,
52 journals: Vec<ExecutionSettlementJournalRecord>,
53}
54#[derive(Deserialize)]
55#[serde(deny_unknown_fields)]
56struct SettlementFields {
57 version: u16,
58 plan_intent: ArtifactChecksumRecord,
59 #[serde(deserialize_with = "bounded_journals")]
60 journals: Vec<ExecutionSettlementJournalRecord>,
61}
62impl TryFrom<SettlementFields> for ExecutionSettlementRecord {
63 type Error = ExecutionSettlementError;
64 fn try_from(fields: SettlementFields) -> Result<Self, Self::Error> {
65 if fields.version != 1 {
66 return Err(ExecutionSettlementError::UnsupportedVersion(fields.version));
67 }
68 Self::new(fields.plan_intent, fields.journals)
69 }
70}
71impl ExecutionSettlementRecord {
72 pub fn new(
79 plan_intent: ArtifactChecksumRecord,
80 mut journals: Vec<ExecutionSettlementJournalRecord>,
81 ) -> Result<Self, ExecutionSettlementError> {
82 if journals.is_empty() || journals.len() > MAX_EFFECT_OPERATIONS {
83 return Err(ExecutionSettlementError::InvalidJournalCount);
84 }
85 journals.sort_by_key(ExecutionSettlementJournalRecord::operation_sequence);
86 if journals
87 .windows(2)
88 .any(|rows| rows[0].operation_sequence == rows[1].operation_sequence)
89 {
90 return Err(ExecutionSettlementError::DuplicateOperation);
91 }
92 Ok(Self {
93 version: 1,
94 plan_intent,
95 journals,
96 })
97 }
98 #[must_use]
100 pub const fn plan_intent(&self) -> &ArtifactChecksumRecord {
101 &self.plan_intent
102 }
103 #[must_use]
105 pub fn journals(&self) -> &[ExecutionSettlementJournalRecord] {
106 &self.journals
107 }
108 #[must_use]
111 pub fn digest(&self) -> ArtifactChecksumRecord {
112 let mut bytes = b"ic-backup/execution-settlement/v1\0".to_vec();
113 bytes.extend_from_slice(self.plan_intent.hash().as_bytes());
114 bytes.extend_from_slice(&(self.journals.len() as u64).to_be_bytes());
115 for row in &self.journals {
116 bytes.extend_from_slice(&row.operation_sequence.to_be_bytes());
117 bytes.extend_from_slice(row.history.hash().as_bytes());
118 }
119 ArtifactChecksumRecord::from_bytes(&bytes)
120 }
121}
122fn bounded_journals<'de, D: Deserializer<'de>>(
123 deserializer: D,
124) -> Result<Vec<ExecutionSettlementJournalRecord>, D::Error> {
125 bounded_sequence::deserialize::<_, _, MAX_EFFECT_OPERATIONS>(
126 deserializer,
127 format!("at most {MAX_EFFECT_OPERATIONS} original journal fingerprints"),
128 "too many journal fingerprints",
129 )
130}
131#[derive(Debug, Eq, Error, PartialEq)]
133pub enum ExecutionSettlementError {
134 #[error("unsupported execution settlement version {0}")]
136 UnsupportedVersion(u16),
137 #[error("invalid execution settlement journal count")]
139 InvalidJournalCount,
140 #[error("duplicate execution settlement operation")]
142 DuplicateOperation,
143}
144#[cfg(test)]
145mod tests;