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}
12
13pub trait CloudSyncRotationStateAccess: Send + Sync {
14    fn mark_candidate(
15        &self,
16        generation: u64,
17        mutation: ObjectHash,
18    ) -> Result<(), RotationStateError>;
19    fn mark_committed_mutation(
20        &self,
21        generation: u64,
22        mutation: ObjectHash,
23    ) -> Result<(), RotationStateError>;
24    fn gate(&self) -> Option<RotationGate>;
25    fn install_durable_gate(&self, gate: Option<RotationGate>);
26    fn check(&self, live_generation: Option<u64>) -> Result<(), RotationPending>;
27}
28
29impl Default for PendingRotation {
30    fn default() -> Self {
31        Self(std::sync::RwLock::new(None))
32    }
33}
34
35impl PendingRotation {
36    pub fn none() -> Self {
37        Self::default()
38    }
39
40    pub fn mark_candidate(
41        &self,
42        generation: u64,
43        mutation: coven_protocol::store_commit::ObjectHash,
44    ) -> Result<(), RotationStateError> {
45        let mut recorded = self
46            .0
47            .write()
48            .map_err(|_| RotationStateError::LockPoisoned)?;
49        *recorded = Some(RotationGate::with_candidate(
50            recorded.clone(),
51            generation,
52            mutation,
53        )?);
54        Ok(())
55    }
56
57    pub fn mark_committed_mutation(
58        &self,
59        generation: u64,
60        mutation: coven_protocol::store_commit::ObjectHash,
61    ) -> Result<(), RotationStateError> {
62        let mut recorded = self
63            .0
64            .write()
65            .map_err(|_| RotationStateError::LockPoisoned)?;
66        *recorded = Some(RotationGate::commit_candidate(
67            recorded.clone(),
68            generation,
69            mutation,
70        )?);
71        Ok(())
72    }
73
74    pub fn gate(&self) -> Option<RotationGate> {
75        self.0.read().unwrap().clone()
76    }
77
78    pub fn install_durable_gate(&self, gate: Option<RotationGate>) {
79        *self.0.write().unwrap() = gate;
80    }
81
82    /// Check the live generation against the committed generation, if one is pending. A
83    /// plaintext home never rotates a store key (sharing, and hence removal,
84    /// requires an encrypted home), so it is never blocked.
85    pub fn check(&self, live_generation: Option<u64>) -> Result<(), RotationPending> {
86        let Some(live_generation) = live_generation else {
87            return Ok(());
88        };
89        if let Some(gate) = self.gate() {
90            return Err(RotationPending {
91                state: gate.pending_state(),
92                live_generation,
93            });
94        }
95        Ok(())
96    }
97
98    /// Record that the cloud has committed `generation` and this device has not
99    /// folded it into its live cipher. Forward-only: a generation not newer than
100    /// one already recorded leaves the recorded value untouched, so an older
101    /// rediscovery (e.g. a decoy wrap from a non-rotating owner) can never erase
102    /// a genuinely newer generation already known to be pending.
103    #[cfg(any(test, feature = "test-utils"))]
104    pub fn mark_committed(&self, generation: u64) -> Result<(), RotationStateError> {
105        let mut recorded = self
106            .0
107            .write()
108            .map_err(|_| RotationStateError::LockPoisoned)?;
109        *recorded = Some(RotationGate::merge_peer_commit(
110            recorded.clone(),
111            generation,
112        )?);
113        Ok(())
114    }
115
116    /// The recorded committed generation, if any is pending — for status
117    /// reporting independent of a specific cipher snapshot.
118    #[cfg(any(test, feature = "test-utils"))]
119    pub fn pending_generation(&self) -> Option<u64> {
120        self.0
121            .read()
122            .unwrap()
123            .as_ref()
124            .map(|gate| gate.generation().get())
125    }
126}
127
128impl CloudSyncRotationStateAccess for PendingRotation {
129    fn mark_candidate(
130        &self,
131        generation: u64,
132        mutation: ObjectHash,
133    ) -> Result<(), RotationStateError> {
134        PendingRotation::mark_candidate(self, generation, mutation)
135    }
136
137    fn mark_committed_mutation(
138        &self,
139        generation: u64,
140        mutation: ObjectHash,
141    ) -> Result<(), RotationStateError> {
142        PendingRotation::mark_committed_mutation(self, generation, mutation)
143    }
144
145    fn gate(&self) -> Option<RotationGate> {
146        PendingRotation::gate(self)
147    }
148
149    fn install_durable_gate(&self, gate: Option<RotationGate>) {
150        PendingRotation::install_durable_gate(self, gate);
151    }
152
153    fn check(&self, live_generation: Option<u64>) -> Result<(), RotationPending> {
154        PendingRotation::check(self, live_generation)
155    }
156}