Skip to main content

coven_database/store/store_session/
membership_rotation.rs

1use super::{StoreDatabase, StoreSession};
2use crate::DbError;
3use coven_protocol::objects::RotationGate;
4use coven_protocol::store_commit::ObjectHash;
5
6impl StoreSession<'_> {
7    fn load_rotation_gate(&mut self) -> Result<Option<RotationGate>, DbError> {
8        load_rotation_gate_on(self.conn).map(|gate| gate.map(|(_, gate)| gate))
9    }
10
11    fn record_peer_rotation(&mut self, generation: u64) -> Result<RotationGate, DbError> {
12        let conn = self.conn;
13        let tx = conn.unchecked_transaction().map_err(DbError::from)?;
14        let existing = load_rotation_gate_on(&tx)?;
15        let next = RotationGate::merge_peer_commit(
16            existing.as_ref().map(|(_, gate)| gate.clone()),
17            generation,
18        )
19        .map_err(DbError::from)?;
20        replace_rotation_gate_on(
21            &tx,
22            existing.as_ref(),
23            Some(next.clone()),
24            "peer rotation recording",
25        )?;
26        tx.commit().map_err(DbError::from)?;
27        Ok(next)
28    }
29
30    fn complete_peer_rotation_adoption(
31        &mut self,
32        adopted_generation: u64,
33    ) -> Result<Option<RotationGate>, DbError> {
34        let conn = self.conn;
35        let tx = conn.unchecked_transaction().map_err(DbError::from)?;
36        let existing = load_rotation_gate_on(&tx)?.ok_or_else(|| {
37            DbError::Message("rotation gate is absent during peer rotation adoption".to_string())
38        })?;
39        let next = existing
40            .1
41            .clone()
42            .complete_peer_adoption(adopted_generation)
43            .map_err(DbError::from)?;
44        replace_rotation_gate_on(&tx, Some(&existing), next.clone(), "peer rotation adoption")?;
45        tx.commit().map_err(DbError::from)?;
46        Ok(next)
47    }
48
49    fn complete_local_rotation_adoption(
50        &mut self,
51        intent_hash: ObjectHash,
52        generation: u64,
53    ) -> Result<Option<RotationGate>, DbError> {
54        let conn = self.conn;
55        let tx = conn.unchecked_transaction().map_err(DbError::from)?;
56        let existing = load_rotation_gate_on(&tx)?.ok_or_else(|| {
57            DbError::Message("rotation gate is absent during local rotation adoption".to_string())
58        })?;
59        let next = existing
60            .1
61            .clone()
62            .complete_local_adoption(generation, intent_hash)
63            .map_err(DbError::from)?;
64        if tx
65            .execute(
66                "DELETE FROM outbound_membership_mutation \
67                 WHERE singleton = 1 AND intent_hash = ?1",
68                [intent_hash.to_string()],
69            )
70            .map_err(DbError::from)?
71            != 1
72        {
73            return Err(DbError::Message(
74                "membership mutation changed during local rotation adoption".to_string(),
75            ));
76        }
77        replace_rotation_gate_on(
78            &tx,
79            Some(&existing),
80            next.clone(),
81            "local rotation adoption",
82        )?;
83        tx.commit().map_err(DbError::from)?;
84        Ok(next)
85    }
86}
87
88impl StoreDatabase {
89    pub async fn load_rotation_gate(&self) -> Result<Option<RotationGate>, DbError> {
90        self.call_store(|session| session.load_rotation_gate())
91            .await
92    }
93
94    pub async fn record_peer_rotation(&self, generation: u64) -> Result<RotationGate, DbError> {
95        self.call_store(move |session| session.record_peer_rotation(generation))
96            .await
97    }
98
99    pub async fn complete_peer_rotation_adoption(
100        &self,
101        adopted_generation: u64,
102    ) -> Result<Option<RotationGate>, DbError> {
103        self.call_store(move |session| session.complete_peer_rotation_adoption(adopted_generation))
104            .await
105    }
106
107    pub async fn complete_local_rotation_adoption(
108        &self,
109        intent_hash: ObjectHash,
110        generation: u64,
111    ) -> Result<Option<RotationGate>, DbError> {
112        self.call_store(move |session| {
113            session.complete_local_rotation_adoption(intent_hash, generation)
114        })
115        .await
116    }
117}
118
119pub(super) fn stage_pending_rotation_on(
120    tx: &rusqlite::Transaction<'_>,
121    generation: Option<u64>,
122    mutation: ObjectHash,
123) -> Result<(), DbError> {
124    let Some(generation) = generation else {
125        return Ok(());
126    };
127    let existing = load_rotation_gate_on(tx)?;
128    let gate = RotationGate::with_candidate(
129        existing.as_ref().map(|(_, gate)| gate.clone()),
130        generation,
131        mutation,
132    )
133    .map_err(DbError::from)?;
134    replace_rotation_gate_on(tx, existing.as_ref(), Some(gate), "candidate staging")
135}
136
137pub(super) fn replace_rotation_candidate_mutation_on(
138    tx: &rusqlite::Transaction<'_>,
139    previous: ObjectHash,
140    replacement: ObjectHash,
141    generation: u64,
142) -> Result<(), DbError> {
143    let existing = load_rotation_gate_on(tx)?.ok_or_else(|| {
144        DbError::Message("rotation gate is absent during candidate replacement".to_string())
145    })?;
146    let next = existing
147        .1
148        .clone()
149        .replace_candidate_mutation(generation, previous, replacement)
150        .map_err(DbError::from)?;
151    replace_rotation_gate_on(tx, Some(&existing), Some(next), "candidate replacement")
152}
153
154pub(super) fn remove_rotation_candidate_on(
155    tx: &rusqlite::Transaction<'_>,
156    intent_hash: ObjectHash,
157    generation: u64,
158) -> Result<(), DbError> {
159    let existing = load_rotation_gate_on(tx)?.ok_or_else(|| {
160        DbError::Message("rotation gate is absent during candidate loss".to_string())
161    })?;
162    let next = existing
163        .1
164        .clone()
165        .remove_candidate(generation, intent_hash)
166        .map_err(DbError::from)?;
167    replace_rotation_gate_on(tx, Some(&existing), next, "candidate loss")
168}
169
170pub(super) fn commit_rotation_candidate_on(
171    tx: &rusqlite::Transaction<'_>,
172    intent_hash: ObjectHash,
173    generation: u64,
174) -> Result<(), DbError> {
175    let existing = load_rotation_gate_on(tx)?.ok_or_else(|| {
176        DbError::Message("rotation gate is absent during candidate activation".to_string())
177    })?;
178    let gate = RotationGate::commit_candidate(Some(existing.1.clone()), generation, intent_hash)
179        .map_err(DbError::from)?;
180    replace_rotation_gate_on(tx, Some(&existing), Some(gate), "membership activation")
181}
182
183fn load_rotation_gate_on(
184    connection: &rusqlite::Connection,
185) -> Result<Option<(String, RotationGate)>, DbError> {
186    let key = coven_protocol::objects::ROTATION_GATE_STATE_KEY;
187    crate::get_protocol_state_on(connection, key)?
188        .map(|encoded| {
189            let gate = serde_json::from_str::<RotationGate>(&encoded)
190                .map_err(|error| DbError::context("parse rotation gate", error))?;
191            Ok((encoded, gate))
192        })
193        .transpose()
194}
195
196fn replace_rotation_gate_on(
197    tx: &rusqlite::Transaction<'_>,
198    expected: Option<&(String, RotationGate)>,
199    next: Option<RotationGate>,
200    operation: &'static str,
201) -> Result<(), DbError> {
202    let key = coven_protocol::objects::ROTATION_GATE_STATE_KEY;
203    let changed = match (expected, next) {
204        (Some((expected, _)), Some(next)) => {
205            let encoded = serde_json::to_string(&next).map_err(|error| {
206                DbError::context(format!("serialize rotation gate during {operation}"), error)
207            })?;
208            tx.execute(
209                "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
210                (&encoded, key, expected),
211            )
212            .map_err(DbError::from)?
213        }
214        (Some((expected, _)), None) => tx
215            .execute(
216                "DELETE FROM protocol_state WHERE key = ?1 AND value = ?2",
217                (key, expected),
218            )
219            .map_err(DbError::from)?,
220        (None, Some(next)) => {
221            let encoded = serde_json::to_string(&next).map_err(|error| {
222                DbError::context(format!("serialize rotation gate during {operation}"), error)
223            })?;
224            tx.execute(
225                "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
226                (key, &encoded),
227            )
228            .map_err(DbError::from)?
229        }
230        (None, None) => return Ok(()),
231    };
232    if changed != 1 {
233        return Err(DbError::Message(format!(
234            "rotation gate changed during {operation}"
235        )));
236    }
237    Ok(())
238}