ic_backup/model/attempt_journal/authority/
mod.rs1use super::AttemptJournalRecordError;
4use crate::model::artifacts::{ArtifactChecksumRecord, canonical_hash};
5use serde::{Deserialize, Serialize};
6
7pub const MAX_OPERATION_ATTEMPTS: u32 = 1024;
9
10#[derive(Clone, Debug)]
12pub struct OperationBindingRequest {
13 pub intent: String,
15 pub operation_sequence: u64,
17 pub network: String,
19 pub caller: String,
21 pub target: String,
23 pub release: String,
25 pub request: String,
27}
28
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
31#[serde(try_from = "BindingFields")]
32pub struct OperationBindingRecord {
33 intent: String,
34 operation_sequence: u64,
35 network: String,
36 caller: String,
37 target: String,
38 release: String,
39 request: String,
40}
41
42#[derive(Deserialize)]
43#[serde(deny_unknown_fields)]
44struct BindingFields {
45 intent: String,
46 operation_sequence: u64,
47 network: String,
48 caller: String,
49 target: String,
50 release: String,
51 request: String,
52}
53
54impl TryFrom<BindingFields> for OperationBindingRecord {
55 type Error = AttemptJournalRecordError;
56 fn try_from(fields: BindingFields) -> Result<Self, Self::Error> {
57 Self::new(&OperationBindingRequest {
58 intent: fields.intent,
59 operation_sequence: fields.operation_sequence,
60 network: fields.network,
61 caller: fields.caller,
62 target: fields.target,
63 release: fields.release,
64 request: fields.request,
65 })
66 }
67}
68
69impl OperationBindingRecord {
70 pub fn new(request: &OperationBindingRequest) -> Result<Self, AttemptJournalRecordError> {
75 Ok(Self {
76 intent: canonical_hash(&request.intent)?,
77 operation_sequence: request.operation_sequence,
78 network: canonical_hash(&request.network)?,
79 caller: crate::model::principal::canonical_text(&request.caller)
80 .ok_or(AttemptJournalRecordError::InvalidPrincipal)?,
81 target: crate::model::principal::canonical_text(&request.target)
82 .ok_or(AttemptJournalRecordError::InvalidPrincipal)?,
83 release: canonical_hash(&request.release)?,
84 request: canonical_hash(&request.request)?,
85 })
86 }
87 #[must_use]
89 pub fn intent(&self) -> &str {
90 &self.intent
91 }
92 #[must_use]
94 pub const fn operation_sequence(&self) -> u64 {
95 self.operation_sequence
96 }
97 #[must_use]
99 pub fn network(&self) -> &str {
100 &self.network
101 }
102 #[must_use]
104 pub fn caller(&self) -> &str {
105 &self.caller
106 }
107 #[must_use]
109 pub fn target(&self) -> &str {
110 &self.target
111 }
112 #[must_use]
114 pub fn release(&self) -> &str {
115 &self.release
116 }
117 #[must_use]
119 pub fn request(&self) -> &str {
120 &self.request
121 }
122}
123
124#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
126#[serde(try_from = "BudgetFields")]
127pub struct AttemptBudgetRecord {
128 mutations: u32,
129 observations: u32,
130}
131
132#[derive(Deserialize)]
133#[serde(deny_unknown_fields)]
134struct BudgetFields {
135 mutations: u32,
136 observations: u32,
137}
138
139impl TryFrom<BudgetFields> for AttemptBudgetRecord {
140 type Error = AttemptJournalRecordError;
141 fn try_from(fields: BudgetFields) -> Result<Self, Self::Error> {
142 Self::new(fields.mutations, fields.observations)
143 }
144}
145
146impl AttemptBudgetRecord {
147 pub fn new(mutations: u32, observations: u32) -> Result<Self, AttemptJournalRecordError> {
152 if mutations
153 .checked_add(observations)
154 .is_none_or(|total| total > MAX_OPERATION_ATTEMPTS)
155 {
156 return Err(AttemptJournalRecordError::BudgetTooLarge);
157 }
158 Ok(Self {
159 mutations,
160 observations,
161 })
162 }
163 #[must_use]
165 pub const fn mutations(&self) -> u32 {
166 self.mutations
167 }
168 #[must_use]
170 pub const fn observations(&self) -> u32 {
171 self.observations
172 }
173}
174
175#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
179#[serde(deny_unknown_fields)]
180pub struct AttemptAuthorityRecord {
181 binding: OperationBindingRecord,
182 budget: AttemptBudgetRecord,
183}
184
185impl AttemptAuthorityRecord {
186 #[must_use]
188 pub const fn new(binding: OperationBindingRecord, budget: AttemptBudgetRecord) -> Self {
189 Self { binding, budget }
190 }
191 #[must_use]
193 pub const fn binding(&self) -> &OperationBindingRecord {
194 &self.binding
195 }
196 #[must_use]
198 pub const fn budget(&self) -> &AttemptBudgetRecord {
199 &self.budget
200 }
201 #[must_use]
208 pub fn digest(&self) -> ArtifactChecksumRecord {
209 let mut bytes = b"ic-backup/attempt-authority/v1\0".to_vec();
210 for digest in [
211 &self.binding.intent,
212 &self.binding.network,
213 &self.binding.release,
214 &self.binding.request,
215 ] {
216 bytes.extend_from_slice(digest.as_bytes());
217 }
218 bytes.extend_from_slice(&self.binding.operation_sequence.to_be_bytes());
219 for principal in [&self.binding.caller, &self.binding.target] {
220 bytes.push(principal.len().to_le_bytes()[0]);
222 bytes.extend_from_slice(principal.as_bytes());
223 }
224 bytes.extend_from_slice(&self.budget.mutations.to_be_bytes());
225 bytes.extend_from_slice(&self.budget.observations.to_be_bytes());
226 ArtifactChecksumRecord::from_bytes(&bytes)
227 }
228}