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