1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
8#[error(
9 "store-key rotation is pending ({state:?}) while this device is sealing under generation \
10 {live_generation}; refusing to seal for the cloud until the pending state is completed"
11)]
12pub struct RotationPending {
13 pub state: RotationPendingState,
14 pub live_generation: u64,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum RotationPendingState {
19 Candidate {
20 generation: u64,
21 },
22 LocalCommitted {
23 generation: u64,
24 },
25 PeerCommitted {
26 generation: u64,
27 },
28 CandidateAndPeer {
29 candidate_generation: u64,
30 peer_generation: u64,
31 },
32 LocalCommittedAndPeer {
33 local_generation: u64,
34 peer_generation: u64,
35 },
36}
37
38pub const ROTATION_GATE_STATE_KEY: &str = "rotation_gate";
54
55#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
56#[serde(rename_all = "snake_case", deny_unknown_fields)]
57pub enum RotationGate {
58 Local(LocalRotation),
60 Peer { generation: NonZeroU64 },
63 LocalAndPeer {
66 local: LocalRotation,
67 peer_generation: NonZeroU64,
68 },
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
76#[serde(rename_all = "snake_case", deny_unknown_fields)]
77pub enum LocalRotation {
78 Candidate {
79 generation: NonZeroU64,
80 mutation: crate::store_commit::ObjectHash,
81 },
82 Committed {
83 generation: NonZeroU64,
84 mutation: crate::store_commit::ObjectHash,
85 },
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
89pub enum RotationGateError {
90 #[error("rotation candidate names generation zero")]
91 CandidateGenerationZero,
92 #[error("a committed local rotation already owns the gate")]
93 CommittedLocalOwnsGate,
94 #[error("another rotation candidate already owns the gate")]
95 DifferentCandidateOwnsGate,
96 #[error("rotation commit does not own the pending candidate gate")]
97 CommitDoesNotOwnCandidate,
98 #[error("committed rotation names generation zero")]
99 CommittedGenerationZero,
100 #[error("rotation loss does not own the pending candidate gate")]
101 LossDoesNotOwnCandidate,
102 #[error("rotation candidate replacement lost its exact owner")]
103 ReplacementLostOwner,
104 #[error("rotation adoption cannot close while a candidate is pending")]
105 CandidatePendingDuringAdoption,
106 #[error("rotation adoption does not own the committed gate")]
107 AdoptionDoesNotOwnCommitted,
108 #[error("adopted rotation names generation zero")]
109 AdoptedGenerationZero,
110}
111
112impl LocalRotation {
113 #[cfg(any(test, feature = "test-utils"))]
117 fn generation(&self) -> NonZeroU64 {
118 match self {
119 Self::Candidate { generation, .. } | Self::Committed { generation, .. } => *generation,
120 }
121 }
122}
123
124impl RotationGate {
125 fn local(&self) -> Option<LocalRotation> {
127 match self {
128 Self::Local(local) | Self::LocalAndPeer { local, .. } => Some(*local),
129 Self::Peer { .. } => None,
130 }
131 }
132
133 fn peer(&self) -> Option<NonZeroU64> {
135 match self {
136 Self::Peer { generation }
137 | Self::LocalAndPeer {
138 peer_generation: generation,
139 ..
140 } => Some(*generation),
141 Self::Local(_) => None,
142 }
143 }
144
145 fn from_parts(local: Option<LocalRotation>, peer: Option<NonZeroU64>) -> Option<Self> {
148 match (local, peer) {
149 (Some(local), Some(peer_generation)) => Some(Self::LocalAndPeer {
150 local,
151 peer_generation,
152 }),
153 (Some(local), None) => Some(Self::Local(local)),
154 (None, Some(generation)) => Some(Self::Peer { generation }),
155 (None, None) => None,
156 }
157 }
158
159 fn with_local(local: LocalRotation, peer: Option<NonZeroU64>) -> Self {
161 match peer {
162 Some(peer_generation) => Self::LocalAndPeer {
163 local,
164 peer_generation,
165 },
166 None => Self::Local(local),
167 }
168 }
169
170 pub fn pending_state(&self) -> RotationPendingState {
171 match self {
172 Self::Local(LocalRotation::Candidate { generation, .. }) => {
173 RotationPendingState::Candidate {
174 generation: generation.get(),
175 }
176 }
177 Self::Local(LocalRotation::Committed { generation, .. }) => {
178 RotationPendingState::LocalCommitted {
179 generation: generation.get(),
180 }
181 }
182 Self::Peer { generation } => RotationPendingState::PeerCommitted {
183 generation: generation.get(),
184 },
185 Self::LocalAndPeer {
186 local: LocalRotation::Candidate { generation, .. },
187 peer_generation,
188 } => RotationPendingState::CandidateAndPeer {
189 candidate_generation: generation.get(),
190 peer_generation: peer_generation.get(),
191 },
192 Self::LocalAndPeer {
193 local: LocalRotation::Committed { generation, .. },
194 peer_generation,
195 } => RotationPendingState::LocalCommittedAndPeer {
196 local_generation: generation.get(),
197 peer_generation: peer_generation.get(),
198 },
199 }
200 }
201
202 pub fn with_candidate(
205 gate: Option<Self>,
206 generation: u64,
207 mutation: crate::store_commit::ObjectHash,
208 ) -> Result<Self, RotationGateError> {
209 let Some(generation) = NonZeroU64::new(generation) else {
210 return Err(RotationGateError::CandidateGenerationZero);
211 };
212 let candidate = LocalRotation::Candidate {
213 generation,
214 mutation,
215 };
216 match gate.as_ref().and_then(Self::local) {
217 Some(LocalRotation::Committed { .. }) => Err(RotationGateError::CommittedLocalOwnsGate),
218 Some(existing) if existing != candidate => {
219 Err(RotationGateError::DifferentCandidateOwnsGate)
220 }
221 _ => Ok(Self::with_local(
222 candidate,
223 gate.as_ref().and_then(Self::peer),
224 )),
225 }
226 }
227
228 pub fn commit_candidate(
230 gate: Option<Self>,
231 generation: u64,
232 mutation: crate::store_commit::ObjectHash,
233 ) -> Result<Self, RotationGateError> {
234 let Some(generation) = NonZeroU64::new(generation) else {
235 return Err(RotationGateError::CommitDoesNotOwnCandidate);
236 };
237 let committed = LocalRotation::Committed {
238 generation,
239 mutation,
240 };
241 let local = gate.as_ref().and_then(Self::local);
242 if local
245 != Some(LocalRotation::Candidate {
246 generation,
247 mutation,
248 })
249 && local != Some(committed)
250 {
251 return Err(RotationGateError::CommitDoesNotOwnCandidate);
252 }
253 Ok(Self::with_local(
254 committed,
255 gate.as_ref().and_then(Self::peer),
256 ))
257 }
258
259 pub fn merge_peer_commit(
262 gate: Option<Self>,
263 generation: u64,
264 ) -> Result<Self, RotationGateError> {
265 let Some(generation) = NonZeroU64::new(generation) else {
266 return Err(RotationGateError::CommittedGenerationZero);
267 };
268 let peer_generation = gate
269 .as_ref()
270 .and_then(Self::peer)
271 .map_or(generation, |recorded| recorded.max(generation));
272 Ok(match gate.as_ref().and_then(Self::local) {
273 Some(local) => Self::LocalAndPeer {
274 local,
275 peer_generation,
276 },
277 None => Self::Peer {
278 generation: peer_generation,
279 },
280 })
281 }
282
283 pub fn remove_candidate(
284 self,
285 generation: u64,
286 mutation: crate::store_commit::ObjectHash,
287 ) -> Result<Option<Self>, RotationGateError> {
288 let lost = NonZeroU64::new(generation).map(|generation| LocalRotation::Candidate {
289 generation,
290 mutation,
291 });
292 if lost.is_none() || self.local() != lost {
293 return Err(RotationGateError::LossDoesNotOwnCandidate);
294 }
295 Ok(Self::from_parts(None, self.peer()))
296 }
297
298 pub fn replace_candidate_mutation(
299 self,
300 generation: u64,
301 previous: crate::store_commit::ObjectHash,
302 replacement: crate::store_commit::ObjectHash,
303 ) -> Result<Self, RotationGateError> {
304 let Some(generation) = NonZeroU64::new(generation) else {
305 return Err(RotationGateError::ReplacementLostOwner);
306 };
307 if self.local()
308 != Some(LocalRotation::Candidate {
309 generation,
310 mutation: previous,
311 })
312 {
313 return Err(RotationGateError::ReplacementLostOwner);
314 }
315 Ok(Self::with_local(
316 LocalRotation::Candidate {
317 generation,
318 mutation: replacement,
319 },
320 self.peer(),
321 ))
322 }
323
324 pub fn complete_local_adoption(
325 self,
326 generation: u64,
327 mutation: crate::store_commit::ObjectHash,
328 ) -> Result<Option<Self>, RotationGateError> {
329 match self.local() {
330 Some(LocalRotation::Candidate { .. }) => {
331 return Err(RotationGateError::CandidatePendingDuringAdoption)
332 }
333 Some(LocalRotation::Committed {
334 generation: committed,
335 mutation: committed_mutation,
336 }) if committed.get() == generation && committed_mutation == mutation => {}
337 _ => return Err(RotationGateError::AdoptionDoesNotOwnCommitted),
338 }
339 Ok(Self::from_parts(
342 None,
343 self.peer().filter(|peer| peer.get() > generation),
344 ))
345 }
346
347 pub fn complete_peer_adoption(
348 self,
349 adopted_generation: u64,
350 ) -> Result<Option<Self>, RotationGateError> {
351 if adopted_generation == 0 {
352 return Err(RotationGateError::AdoptedGenerationZero);
353 }
354 Ok(Self::from_parts(
355 self.local(),
356 self.peer().filter(|peer| peer.get() > adopted_generation),
357 ))
358 }
359
360 #[cfg(any(test, feature = "test-utils"))]
363 pub fn generation(&self) -> NonZeroU64 {
364 match self {
365 Self::Local(local) => local.generation(),
366 Self::Peer { generation } => *generation,
367 Self::LocalAndPeer {
368 local,
369 peer_generation,
370 } => local.generation().max(*peer_generation),
371 }
372 }
373}