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