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 adoption cannot close while a candidate is pending")]
103 CandidatePendingDuringAdoption,
104 #[error("rotation adoption does not own the committed gate")]
105 AdoptionDoesNotOwnCommitted,
106 #[error("adopted rotation names generation zero")]
107 AdoptedGenerationZero,
108}
109
110impl LocalRotation {
111 #[cfg(any(test, feature = "test-utils"))]
115 fn generation(&self) -> NonZeroU64 {
116 match self {
117 Self::Candidate { generation, .. } | Self::Committed { generation, .. } => *generation,
118 }
119 }
120}
121
122impl RotationGate {
123 fn local(&self) -> Option<LocalRotation> {
125 match self {
126 Self::Local(local) | Self::LocalAndPeer { local, .. } => Some(*local),
127 Self::Peer { .. } => None,
128 }
129 }
130
131 fn peer(&self) -> Option<NonZeroU64> {
133 match self {
134 Self::Peer { generation }
135 | Self::LocalAndPeer {
136 peer_generation: generation,
137 ..
138 } => Some(*generation),
139 Self::Local(_) => None,
140 }
141 }
142
143 fn from_parts(local: Option<LocalRotation>, peer: Option<NonZeroU64>) -> Option<Self> {
146 match (local, peer) {
147 (Some(local), Some(peer_generation)) => Some(Self::LocalAndPeer {
148 local,
149 peer_generation,
150 }),
151 (Some(local), None) => Some(Self::Local(local)),
152 (None, Some(generation)) => Some(Self::Peer { generation }),
153 (None, None) => None,
154 }
155 }
156
157 fn with_local(local: LocalRotation, peer: Option<NonZeroU64>) -> Self {
159 match peer {
160 Some(peer_generation) => Self::LocalAndPeer {
161 local,
162 peer_generation,
163 },
164 None => Self::Local(local),
165 }
166 }
167
168 pub fn pending_state(&self) -> RotationPendingState {
169 match self {
170 Self::Local(LocalRotation::Candidate { generation, .. }) => {
171 RotationPendingState::Candidate {
172 generation: generation.get(),
173 }
174 }
175 Self::Local(LocalRotation::Committed { generation, .. }) => {
176 RotationPendingState::LocalCommitted {
177 generation: generation.get(),
178 }
179 }
180 Self::Peer { generation } => RotationPendingState::PeerCommitted {
181 generation: generation.get(),
182 },
183 Self::LocalAndPeer {
184 local: LocalRotation::Candidate { generation, .. },
185 peer_generation,
186 } => RotationPendingState::CandidateAndPeer {
187 candidate_generation: generation.get(),
188 peer_generation: peer_generation.get(),
189 },
190 Self::LocalAndPeer {
191 local: LocalRotation::Committed { generation, .. },
192 peer_generation,
193 } => RotationPendingState::LocalCommittedAndPeer {
194 local_generation: generation.get(),
195 peer_generation: peer_generation.get(),
196 },
197 }
198 }
199
200 pub fn with_candidate(
203 gate: Option<Self>,
204 generation: u64,
205 mutation: crate::store_commit::ObjectHash,
206 ) -> Result<Self, RotationGateError> {
207 let Some(generation) = NonZeroU64::new(generation) else {
208 return Err(RotationGateError::CandidateGenerationZero);
209 };
210 let candidate = LocalRotation::Candidate {
211 generation,
212 mutation,
213 };
214 match gate.as_ref().and_then(Self::local) {
215 Some(LocalRotation::Committed { .. }) => Err(RotationGateError::CommittedLocalOwnsGate),
216 Some(existing) if existing != candidate => {
217 Err(RotationGateError::DifferentCandidateOwnsGate)
218 }
219 _ => Ok(Self::with_local(
220 candidate,
221 gate.as_ref().and_then(Self::peer),
222 )),
223 }
224 }
225
226 pub fn commit_candidate(
228 gate: Option<Self>,
229 generation: u64,
230 mutation: crate::store_commit::ObjectHash,
231 ) -> Result<Self, RotationGateError> {
232 let Some(generation) = NonZeroU64::new(generation) else {
233 return Err(RotationGateError::CommitDoesNotOwnCandidate);
234 };
235 let committed = LocalRotation::Committed {
236 generation,
237 mutation,
238 };
239 let local = gate.as_ref().and_then(Self::local);
240 if local
243 != Some(LocalRotation::Candidate {
244 generation,
245 mutation,
246 })
247 && local != Some(committed)
248 {
249 return Err(RotationGateError::CommitDoesNotOwnCandidate);
250 }
251 Ok(Self::with_local(
252 committed,
253 gate.as_ref().and_then(Self::peer),
254 ))
255 }
256
257 pub fn merge_peer_commit(
260 gate: Option<Self>,
261 generation: u64,
262 ) -> Result<Self, RotationGateError> {
263 let Some(generation) = NonZeroU64::new(generation) else {
264 return Err(RotationGateError::CommittedGenerationZero);
265 };
266 let peer_generation = gate
267 .as_ref()
268 .and_then(Self::peer)
269 .map_or(generation, |recorded| recorded.max(generation));
270 Ok(match gate.as_ref().and_then(Self::local) {
271 Some(local) => Self::LocalAndPeer {
272 local,
273 peer_generation,
274 },
275 None => Self::Peer {
276 generation: peer_generation,
277 },
278 })
279 }
280
281 pub fn remove_candidate(
282 self,
283 generation: u64,
284 mutation: crate::store_commit::ObjectHash,
285 ) -> Result<Option<Self>, RotationGateError> {
286 let lost = NonZeroU64::new(generation).map(|generation| LocalRotation::Candidate {
287 generation,
288 mutation,
289 });
290 if lost.is_none() || self.local() != lost {
291 return Err(RotationGateError::LossDoesNotOwnCandidate);
292 }
293 Ok(Self::from_parts(None, self.peer()))
294 }
295
296 pub fn complete_local_adoption(
297 self,
298 generation: u64,
299 mutation: crate::store_commit::ObjectHash,
300 ) -> Result<Option<Self>, RotationGateError> {
301 match self.local() {
302 Some(LocalRotation::Candidate { .. }) => {
303 return Err(RotationGateError::CandidatePendingDuringAdoption)
304 }
305 Some(LocalRotation::Committed {
306 generation: committed,
307 mutation: committed_mutation,
308 }) if committed.get() == generation && committed_mutation == mutation => {}
309 _ => return Err(RotationGateError::AdoptionDoesNotOwnCommitted),
310 }
311 Ok(Self::from_parts(
314 None,
315 self.peer().filter(|peer| peer.get() > generation),
316 ))
317 }
318
319 pub fn complete_peer_adoption(
320 self,
321 adopted_generation: u64,
322 ) -> Result<Option<Self>, RotationGateError> {
323 if adopted_generation == 0 {
324 return Err(RotationGateError::AdoptedGenerationZero);
325 }
326 Ok(Self::from_parts(
327 self.local(),
328 self.peer().filter(|peer| peer.get() > adopted_generation),
329 ))
330 }
331
332 #[cfg(any(test, feature = "test-utils"))]
335 pub fn generation(&self) -> NonZeroU64 {
336 match self {
337 Self::Local(local) => local.generation(),
338 Self::Peer { generation } => *generation,
339 Self::LocalAndPeer {
340 local,
341 peer_generation,
342 } => local.generation().max(*peer_generation),
343 }
344 }
345}