ic_backup/model/execution_settlement/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord, attempt_journal::AttemptJournalRecord,
5 effect_graph::MAX_EFFECT_OPERATIONS,
6};
7use serde::{Deserialize, Deserializer, Serialize, de};
8use std::fmt;
9use thiserror::Error;
10
11pub const MAX_EXECUTION_SETTLEMENT_BYTES: u64 = 2 * 1024 * 1024;
13
14#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
16#[serde(deny_unknown_fields)]
17pub struct ExecutionSettlementJournalRecord {
18 operation_sequence: u64,
19 history: ArtifactChecksumRecord,
20}
21impl ExecutionSettlementJournalRecord {
22 #[must_use]
24 pub fn from_journal(journal: &AttemptJournalRecord) -> Self {
25 Self {
26 operation_sequence: journal.authority().binding().operation_sequence(),
27 history: journal.digest(),
28 }
29 }
30 #[must_use]
32 pub const fn operation_sequence(&self) -> u64 {
33 self.operation_sequence
34 }
35 #[must_use]
37 pub const fn history(&self) -> &ArtifactChecksumRecord {
38 &self.history
39 }
40}
41
42#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
49#[serde(try_from = "SettlementFields")]
50pub struct ExecutionSettlementRecord {
51 version: u16,
52 plan_intent: ArtifactChecksumRecord,
53 journals: Vec<ExecutionSettlementJournalRecord>,
54}
55#[derive(Deserialize)]
56#[serde(deny_unknown_fields)]
57struct SettlementFields {
58 version: u16,
59 plan_intent: ArtifactChecksumRecord,
60 #[serde(deserialize_with = "bounded_journals")]
61 journals: Vec<ExecutionSettlementJournalRecord>,
62}
63impl TryFrom<SettlementFields> for ExecutionSettlementRecord {
64 type Error = ExecutionSettlementError;
65 fn try_from(fields: SettlementFields) -> Result<Self, Self::Error> {
66 if fields.version != 1 {
67 return Err(ExecutionSettlementError::UnsupportedVersion(fields.version));
68 }
69 Self::new(fields.plan_intent, fields.journals)
70 }
71}
72impl ExecutionSettlementRecord {
73 pub fn new(
80 plan_intent: ArtifactChecksumRecord,
81 mut journals: Vec<ExecutionSettlementJournalRecord>,
82 ) -> Result<Self, ExecutionSettlementError> {
83 if journals.is_empty() || journals.len() > MAX_EFFECT_OPERATIONS {
84 return Err(ExecutionSettlementError::InvalidJournalCount);
85 }
86 journals.sort_by_key(ExecutionSettlementJournalRecord::operation_sequence);
87 if journals
88 .windows(2)
89 .any(|rows| rows[0].operation_sequence == rows[1].operation_sequence)
90 {
91 return Err(ExecutionSettlementError::DuplicateOperation);
92 }
93 Ok(Self {
94 version: 1,
95 plan_intent,
96 journals,
97 })
98 }
99 #[must_use]
101 pub const fn plan_intent(&self) -> &ArtifactChecksumRecord {
102 &self.plan_intent
103 }
104 #[must_use]
106 pub fn journals(&self) -> &[ExecutionSettlementJournalRecord] {
107 &self.journals
108 }
109 #[must_use]
112 pub fn digest(&self) -> ArtifactChecksumRecord {
113 let mut bytes = b"ic-backup/execution-settlement/v1\0".to_vec();
114 bytes.extend_from_slice(self.plan_intent.hash().as_bytes());
115 bytes.extend_from_slice(&(self.journals.len() as u64).to_be_bytes());
116 for row in &self.journals {
117 bytes.extend_from_slice(&row.operation_sequence.to_be_bytes());
118 bytes.extend_from_slice(row.history.hash().as_bytes());
119 }
120 ArtifactChecksumRecord::from_bytes(&bytes)
121 }
122}
123fn bounded_journals<'de, D: Deserializer<'de>>(
124 deserializer: D,
125) -> Result<Vec<ExecutionSettlementJournalRecord>, D::Error> {
126 struct Rows;
127 impl<'de> de::Visitor<'de> for Rows {
128 type Value = Vec<ExecutionSettlementJournalRecord>;
129 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130 write!(
131 formatter,
132 "at most {MAX_EFFECT_OPERATIONS} original journal fingerprints"
133 )
134 }
135 fn visit_seq<A: de::SeqAccess<'de>>(
136 self,
137 mut sequence: A,
138 ) -> Result<Self::Value, A::Error> {
139 let mut rows = Vec::new();
140 while let Some(row) = sequence.next_element()? {
141 if rows.len() == MAX_EFFECT_OPERATIONS {
142 return Err(de::Error::custom("too many journal fingerprints"));
143 }
144 rows.push(row);
145 }
146 Ok(rows)
147 }
148 }
149 deserializer.deserialize_seq(Rows)
150}
151#[derive(Debug, Eq, Error, PartialEq)]
153pub enum ExecutionSettlementError {
154 #[error("unsupported execution settlement version {0}")]
156 UnsupportedVersion(u16),
157 #[error("invalid execution settlement journal count")]
159 InvalidJournalCount,
160 #[error("duplicate execution settlement operation")]
162 DuplicateOperation,
163}
164#[cfg(test)]
165mod tests;