ic_backup/model/execution_settlement/
mod.rs1use crate::model::evidence_digest::EvidenceDigest;
4use crate::model::{
5 artifacts::ArtifactChecksumRecord, attempt_journal::AttemptJournalRecord, bounded_sequence,
6 effect_graph::MAX_EFFECT_OPERATIONS,
7};
8use serde::{Deserialize, Deserializer, Serialize};
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 = EvidenceDigest::new(b"ic-backup/execution-settlement/v1\0");
114 bytes.append(self.plan_intent.hash().as_bytes());
115 bytes.append(&(self.journals.len() as u64).to_be_bytes());
116 for row in &self.journals {
117 bytes.append(&row.operation_sequence.to_be_bytes());
118 bytes.append(row.history.hash().as_bytes());
119 }
120 bytes.finish()
121 }
122}
123fn bounded_journals<'de, D: Deserializer<'de>>(
124 deserializer: D,
125) -> Result<Vec<ExecutionSettlementJournalRecord>, D::Error> {
126 bounded_sequence::deserialize::<_, _, MAX_EFFECT_OPERATIONS>(
127 deserializer,
128 format!("at most {MAX_EFFECT_OPERATIONS} original journal fingerprints"),
129 "too many journal fingerprints",
130 )
131}
132#[derive(Debug, Eq, Error, PartialEq)]
134pub enum ExecutionSettlementError {
135 #[error("unsupported execution settlement version {0}")]
137 UnsupportedVersion(u16),
138 #[error("invalid execution settlement journal count")]
140 InvalidJournalCount,
141 #[error("duplicate execution settlement operation")]
143 DuplicateOperation,
144}
145#[cfg(test)]
146mod tests;