Skip to main content

coven_storage/remote/
rotation.rs

1use super::*;
2
3pub struct PendingRotation(std::sync::RwLock<Option<RotationGate>>);
4
5#[derive(Debug, thiserror::Error)]
6pub enum RotationStateError {
7    #[error("rotation gate transition failed: {0}")]
8    Gate(#[from] coven_protocol::objects::RotationGateError),
9    #[error("rotation state lock is poisoned")]
10    LockPoisoned,
11    #[error("rotation candidate gate is absent during proven nonactivation")]
12    MissingCandidateDuringNonactivation,
13    #[error("rotation candidate gate is absent during candidate replacement")]
14    MissingCandidateDuringReplacement,
15}
16
17pub trait CloudSyncRotationStateAccess: Send + Sync {
18    fn mark_candidate(
19        &self,
20        generation: u64,
21        mutation: ObjectHash,
22    ) -> Result<(), RotationStateError>;
23    fn mark_committed_mutation(
24        &self,
25        generation: u64,
26        mutation: ObjectHash,
27    ) -> Result<(), RotationStateError>;
28    fn remove_candidate(
29        &self,
30        generation: u64,
31        mutation: ObjectHash,
32    ) -> Result<(), RotationStateError>;
33    fn replace_candidate_mutation(
34        &self,
35        generation: u64,
36        previous: ObjectHash,
37        replacement: ObjectHash,
38    ) -> Result<(), RotationStateError>;
39    fn gate(&self) -> Option<RotationGate>;
40    fn install_durable_gate(&self, gate: Option<RotationGate>);
41    fn check(&self, live_generation: Option<u64>) -> Result<(), RotationPending>;
42}
43
44impl Default for PendingRotation {
45    fn default() -> Self {
46        Self(std::sync::RwLock::new(None))
47    }
48}
49
50impl PendingRotation {
51    pub fn none() -> Self {
52        Self::default()
53    }
54
55    pub fn mark_candidate(
56        &self,
57        generation: u64,
58        mutation: coven_protocol::store_commit::ObjectHash,
59    ) -> Result<(), RotationStateError> {
60        let mut recorded = self
61            .0
62            .write()
63            .map_err(|_| RotationStateError::LockPoisoned)?;
64        *recorded = Some(RotationGate::with_candidate(
65            recorded.clone(),
66            generation,
67            mutation,
68        )?);
69        Ok(())
70    }
71
72    pub fn mark_committed_mutation(
73        &self,
74        generation: u64,
75        mutation: coven_protocol::store_commit::ObjectHash,
76    ) -> Result<(), RotationStateError> {
77        let mut recorded = self
78            .0
79            .write()
80            .map_err(|_| RotationStateError::LockPoisoned)?;
81        *recorded = Some(RotationGate::commit_candidate(
82            recorded.clone(),
83            generation,
84            mutation,
85        )?);
86        Ok(())
87    }
88
89    pub fn remove_candidate(
90        &self,
91        generation: u64,
92        mutation: coven_protocol::store_commit::ObjectHash,
93    ) -> Result<(), RotationStateError> {
94        let mut recorded = self
95            .0
96            .write()
97            .map_err(|_| RotationStateError::LockPoisoned)?;
98        let gate = recorded
99            .clone()
100            .ok_or(RotationStateError::MissingCandidateDuringNonactivation)?;
101        *recorded = gate.remove_candidate(generation, mutation)?;
102        Ok(())
103    }
104
105    pub fn replace_candidate_mutation(
106        &self,
107        generation: u64,
108        previous: coven_protocol::store_commit::ObjectHash,
109        replacement: coven_protocol::store_commit::ObjectHash,
110    ) -> Result<(), RotationStateError> {
111        let mut recorded = self
112            .0
113            .write()
114            .map_err(|_| RotationStateError::LockPoisoned)?;
115        let gate = recorded
116            .clone()
117            .ok_or(RotationStateError::MissingCandidateDuringReplacement)?;
118        *recorded = Some(gate.replace_candidate_mutation(generation, previous, replacement)?);
119        Ok(())
120    }
121
122    pub fn gate(&self) -> Option<RotationGate> {
123        self.0.read().unwrap().clone()
124    }
125
126    pub fn install_durable_gate(&self, gate: Option<RotationGate>) {
127        *self.0.write().unwrap() = gate;
128    }
129
130    /// Check the live generation against the committed generation, if one is pending. A
131    /// plaintext home never rotates a store key (sharing, and hence removal,
132    /// requires an encrypted home), so it is never blocked.
133    pub fn check(&self, live_generation: Option<u64>) -> Result<(), RotationPending> {
134        let Some(live_generation) = live_generation else {
135            return Ok(());
136        };
137        if let Some(gate) = self.gate() {
138            return Err(RotationPending {
139                state: gate.pending_state(),
140                live_generation,
141            });
142        }
143        Ok(())
144    }
145
146    /// Record that the cloud has committed `generation` and this device has not
147    /// folded it into its live cipher. Forward-only: a generation not newer than
148    /// one already recorded leaves the recorded value untouched, so an older
149    /// rediscovery (e.g. a decoy wrap from a non-rotating owner) can never erase
150    /// a genuinely newer generation already known to be pending.
151    #[cfg(any(test, feature = "test-utils"))]
152    pub fn mark_committed(&self, generation: u64) -> Result<(), RotationStateError> {
153        let mut recorded = self
154            .0
155            .write()
156            .map_err(|_| RotationStateError::LockPoisoned)?;
157        *recorded = Some(RotationGate::merge_peer_commit(
158            recorded.clone(),
159            generation,
160        )?);
161        Ok(())
162    }
163
164    /// The recorded committed generation, if any is pending — for status
165    /// reporting independent of a specific cipher snapshot.
166    #[cfg(any(test, feature = "test-utils"))]
167    pub fn pending_generation(&self) -> Option<u64> {
168        self.0
169            .read()
170            .unwrap()
171            .as_ref()
172            .map(|gate| gate.generation().get())
173    }
174}
175
176impl CloudSyncRotationStateAccess for PendingRotation {
177    fn mark_candidate(
178        &self,
179        generation: u64,
180        mutation: ObjectHash,
181    ) -> Result<(), RotationStateError> {
182        PendingRotation::mark_candidate(self, generation, mutation)
183    }
184
185    fn mark_committed_mutation(
186        &self,
187        generation: u64,
188        mutation: ObjectHash,
189    ) -> Result<(), RotationStateError> {
190        PendingRotation::mark_committed_mutation(self, generation, mutation)
191    }
192
193    fn remove_candidate(
194        &self,
195        generation: u64,
196        mutation: ObjectHash,
197    ) -> Result<(), RotationStateError> {
198        PendingRotation::remove_candidate(self, generation, mutation)
199    }
200
201    fn replace_candidate_mutation(
202        &self,
203        generation: u64,
204        previous: ObjectHash,
205        replacement: ObjectHash,
206    ) -> Result<(), RotationStateError> {
207        PendingRotation::replace_candidate_mutation(self, generation, previous, replacement)
208    }
209
210    fn gate(&self) -> Option<RotationGate> {
211        PendingRotation::gate(self)
212    }
213
214    fn install_durable_gate(&self, gate: Option<RotationGate>) {
215        PendingRotation::install_durable_gate(self, gate);
216    }
217
218    fn check(&self, live_generation: Option<u64>) -> Result<(), RotationPending> {
219        PendingRotation::check(self, live_generation)
220    }
221}