coven_database/store/store_session/
circle_acknowledgements.rs1use crate::query_mapped_rows;
2use crate::*;
3use coven_protocol::circle::{
4 CircleBootstrapCoverageRef, CircleControlCoord, CircleEpochId, CircleId,
5};
6use coven_protocol::objects::PreparedExactObject;
7use coven_protocol::store_commit::{
8 CircleAck, CircleAckRef, CommitFrontier, StoreDeviceId, StoreDeviceStatus, StoreHistoryCut,
9};
10use rusqlite::OptionalExtension;
11use std::collections::{BTreeMap, BTreeSet};
12
13use super::{StoreDatabase, StoreSession};
14
15pub struct CircleAckPublicationInput {
20 control: CircleControlCoord,
21 epoch_id: CircleEpochId,
22 access: coven_protocol::circle_activation::CircleEpochAccess,
23 seeded_from: Option<CircleBootstrapCoverageRef>,
24}
25
26impl CircleAckPublicationInput {
27 pub fn circle_id(&self) -> CircleId {
28 self.access.circle_id()
29 }
30
31 pub fn control(&self) -> &CircleControlCoord {
32 &self.control
33 }
34
35 pub fn epoch_id(&self) -> CircleEpochId {
36 self.epoch_id
37 }
38
39 pub fn seeded_from(&self) -> Option<&CircleBootstrapCoverageRef> {
40 self.seeded_from.as_ref()
41 }
42
43 pub fn protocol_context(
44 &self,
45 store_root_hash: coven_protocol::store_commit::ObjectHash,
46 domain: coven_protocol::objects::CircleProtocolObjectDomain,
47 ) -> coven_protocol::objects::ProtocolObjectContext {
48 self.access.protocol_context(store_root_hash, domain)
49 }
50
51 pub fn key_fingerprint(&self) -> coven_keys::encryption::KeyFingerprint {
52 self.access.key_fingerprint()
53 }
54}
55
56pub struct PublishedCircleAck {
60 pub reference: CircleAckRef,
61 pub successor_slot: coven_protocol::objects::ObjectSlot,
62 pub store_cut: CommitFrontier,
63 pub control: CircleControlCoord,
64}
65
66impl StoreSession<'_> {
67 fn circle_acknowledgement_publication_inputs(
68 &self,
69 ) -> Result<Vec<CircleAckPublicationInput>, DbError> {
70 let conn = self.conn;
71 let mut inputs = Vec::new();
72 for state in super::circle_operations::circle_current_states_on(conn)? {
73 let circle_id = state.circle_id();
74 let Some(authoring) = state.authoring_state() else {
75 tracing::debug!(
76 circle_id = %circle_id,
77 "skip Circle acknowledgement: recipient holds no active access"
78 );
79 continue;
80 };
81 let control = authoring.control.coord.clone();
82 let epoch_id = authoring.control.value.epoch_id();
83 let access = super::circle_publication_context_on(conn, circle_id, &control)?;
84 let seeded_from =
85 super::retained_merge_replay::circle_bootstrap_coverage_ref_on(conn, circle_id)?;
86 inputs.push(CircleAckPublicationInput {
87 control,
88 epoch_id,
89 access,
90 seeded_from,
91 });
92 }
93 Ok(inputs)
94 }
95
96 fn activated_circle_ack(
97 &self,
98 circle_id: CircleId,
99 device_id: StoreDeviceId,
100 ) -> Result<Option<CircleAckRef>, DbError> {
101 self.conn
102 .query_row(
103 "SELECT ack_ref FROM activated_circle_acks
104 WHERE circle_id = ?1 AND device_id = ?2",
105 rusqlite::params![circle_id.to_string(), device_id.to_string()],
106 |row| row.get::<_, String>(0),
107 )
108 .optional()
109 .map_err(DbError::from)?
110 .map(|raw| parse_circle_ack_ref(&raw, circle_id, "activated"))
111 .transpose()
112 }
113
114 fn circle_current_roster_members(
115 &self,
116 circle_id: CircleId,
117 ) -> Result<BTreeSet<String>, DbError> {
118 let Some(state) = super::circle_operations::circle_current_state_on(self.conn, circle_id)?
119 else {
120 return Err(DbError::Message(format!(
121 "Circle {circle_id} has no current state"
122 )));
123 };
124 let Some((_current, _access, roster, _metadata)) = state.active() else {
125 return Ok(BTreeSet::new());
126 };
127 Ok(roster.members().into_keys().collect())
128 }
129
130 fn activated_circle_acks(&self, circle_id: CircleId) -> Result<Vec<CircleAckRef>, DbError> {
131 query_mapped_rows(
132 self.conn,
133 "SELECT ack_ref FROM activated_circle_acks
134 WHERE circle_id = ?1 ORDER BY device_id",
135 [circle_id.to_string()],
136 |row| row.get::<_, String>(0),
137 )?
138 .into_iter()
139 .map(|raw| parse_circle_ack_ref(&raw, circle_id, "activated"))
140 .collect()
141 }
142
143 fn latest_published_circle_ack(
144 &self,
145 circle_id: CircleId,
146 ) -> Result<Option<PublishedCircleAck>, DbError> {
147 let row: Option<(String, String, String, String)> = self
148 .conn
149 .query_row(
150 "SELECT ack_ref, successor_slot, store_cut, control_coord
151 FROM published_circle_acks WHERE circle_id = ?1",
152 [circle_id.to_string()],
153 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
154 )
155 .optional()
156 .map_err(DbError::from)?;
157 let Some((reference, successor_slot, store_cut, control)) = row else {
158 return Ok(None);
159 };
160 let reference = parse_circle_ack_ref(&reference, circle_id, "published")?;
161 if reference.sequence == 0 {
162 return Err(DbError::Message(
163 "published Circle acknowledgement names sequence zero".to_string(),
164 ));
165 }
166 Ok(Some(PublishedCircleAck {
167 reference,
168 successor_slot: serde_json::from_str(&successor_slot).map_err(|error| {
169 DbError::context("published Circle acknowledgement successor slot", error)
170 })?,
171 store_cut: serde_json::from_str(&store_cut)
172 .map_err(|error| DbError::context("published Circle acknowledgement cut", error))?,
173 control: serde_json::from_str(&control).map_err(|error| {
174 DbError::context("published Circle acknowledgement control", error)
175 })?,
176 }))
177 }
178
179 fn outbound_circle_acks_pending(&self) -> Result<bool, DbError> {
184 self.conn
185 .query_row(
186 "SELECT EXISTS(SELECT 1 FROM outbound_circle_acks)",
187 [],
188 |row| row.get::<_, bool>(0),
189 )
190 .map_err(DbError::from)
191 }
192
193 fn stage_circle_ack(
194 &mut self,
195 ack: CircleAck,
196 prepared: PreparedExactObject,
197 ) -> Result<CircleAckRef, DbError> {
198 let authority = self.local_store_authority()?;
199 let registration = authority.value();
200 let bytes = ack.to_bytes();
201 let reference = CircleAckRef {
202 registration: ack.registration.clone(),
203 circle_id: ack.circle_id,
204 control: ack.control.clone(),
205 sequence: ack.sequence,
206 ack_hash: ack.ack_hash(),
207 object: prepared.reference().clone(),
208 };
209 CircleAck::parse_at(&bytes, ®istration.store_root, &reference, registration)
210 .map_err(|error| DbError::context("stage Circle acknowledgement", error))?;
211 let ack_ref = serde_json::to_string(&reference).map_err(|error| {
212 DbError::context("serialize exact Circle acknowledgement ref", error)
213 })?;
214 let prepared = serde_json::to_string(&prepared).map_err(|error| {
215 DbError::context("serialize prepared Circle acknowledgement", error)
216 })?;
217 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
218 tx.execute(
219 "INSERT INTO outbound_circle_acks (circle_id, ack_ref, ack_bytes, prepared_object)
220 VALUES (?1, ?2, ?3, ?4)",
221 rusqlite::params![reference.circle_id.to_string(), ack_ref, bytes, prepared],
222 )
223 .map_err(DbError::from)?;
224 tx.commit().map_err(DbError::from)?;
225 Ok(reference)
226 }
227}
228
229fn parse_circle_ack_ref(
230 raw: &str,
231 circle_id: CircleId,
232 state: &str,
233) -> Result<CircleAckRef, DbError> {
234 let reference: CircleAckRef = serde_json::from_str(raw)
235 .map_err(|error| DbError::context(format!("{state} Circle acknowledgement ref"), error))?;
236 if reference.circle_id != circle_id {
237 return Err(DbError::Message(format!(
238 "{state} Circle acknowledgement names another Circle"
239 )));
240 }
241 Ok(reference)
242}
243
244impl StoreDatabase {
245 pub async fn circle_acknowledgement_publication_inputs(
246 &self,
247 ) -> Result<Vec<CircleAckPublicationInput>, DbError> {
248 self.call_store(|session| session.circle_acknowledgement_publication_inputs())
249 .await
250 }
251
252 pub async fn activated_circle_ack(
256 &self,
257 circle_id: CircleId,
258 device_id: StoreDeviceId,
259 ) -> Result<Option<CircleAckRef>, DbError> {
260 self.call_store(move |session| session.activated_circle_ack(circle_id, device_id))
261 .await
262 }
263
264 pub async fn active_circle_access_devices(
272 &self,
273 circle_id: CircleId,
274 ) -> Result<BTreeSet<StoreDeviceId>, DbError> {
275 let members = self.circle_current_roster_members(circle_id).await?;
276 if members.is_empty() {
277 return Ok(BTreeSet::new());
278 }
279 let frontier = CommitFrontier::from_refs(self.materialized_frontier().await?)
280 .map_err(|error| DbError::context("shape current Store frontier", error))?;
281 let (_, device_state) = self
282 .store_device_state_for_history_cut(&StoreHistoryCut(frontier.0))
283 .await?;
284 let owners: BTreeMap<StoreDeviceId, String> = self
285 .activated_store_device_registration_records()
286 .await?
287 .into_iter()
288 .map(|registration| {
289 (
290 registration.value().device_id,
291 registration.value().author_pubkey.clone(),
292 )
293 })
294 .collect();
295 let mut devices = BTreeSet::new();
296 for (device_id, record) in device_state.devices {
297 if !matches!(record.status, StoreDeviceStatus::Active) {
298 continue;
299 }
300 let owner = owners.get(&device_id).ok_or_else(|| {
301 DbError::Message(format!(
302 "active Store device {device_id} has no activated registration"
303 ))
304 })?;
305 if members.contains(owner) {
306 devices.insert(device_id);
307 }
308 }
309 Ok(devices)
310 }
311
312 pub async fn circle_current_roster_members(
315 &self,
316 circle_id: CircleId,
317 ) -> Result<BTreeSet<String>, DbError> {
318 self.call_store(move |session| session.circle_current_roster_members(circle_id))
319 .await
320 }
321
322 pub async fn activated_circle_acks(
328 &self,
329 circle_id: CircleId,
330 ) -> Result<Vec<CircleAckRef>, DbError> {
331 self.call_store(move |session| session.activated_circle_acks(circle_id))
332 .await
333 }
334
335 pub async fn latest_published_circle_ack(
336 &self,
337 circle_id: CircleId,
338 ) -> Result<Option<PublishedCircleAck>, DbError> {
339 self.call_store(move |session| session.latest_published_circle_ack(circle_id))
340 .await
341 }
342
343 pub async fn outbound_circle_acks_pending(&self) -> Result<bool, DbError> {
344 self.call_store(|session| session.outbound_circle_acks_pending())
345 .await
346 }
347
348 pub async fn stage_circle_ack(
349 &self,
350 ack: CircleAck,
351 prepared: PreparedExactObject,
352 ) -> Result<CircleAckRef, DbError> {
353 self.call_store(move |session| session.stage_circle_ack(ack, prepared))
354 .await
355 }
356}