1use super::candidate_records::{
11 blocked_merge_candidate_from_prepared, blocked_merge_candidate_nonactivation,
12 merge_candidate_cleanup_targets_on, parse_prepared_merge_candidate_parts_on,
13 terminal_candidate_verification_on, CandidateCleanupObject, PreparedMergeCandidate,
14};
15use super::{StoreDatabase, StoreSession};
16use crate::{
17 candidate_graph_exact_objects, finish_remote_candidate_nonactivation_on,
18 load_protocol_inert_object_on, load_remote_object_on, TerminalCandidateCleanupVerification,
19};
20use coven_protocol::circle::{CircleAccessDisposition, CircleOperationId};
21use coven_protocol::circle_journal::{CircleOperationJournal, PreparedCircleOperation};
22use coven_protocol::objects::ExactObjectRef;
23use coven_protocol::remote_object::remote_object_id;
24use rusqlite::Connection;
25
26struct CircleOperationCandidate {
32 candidate: PreparedMergeCandidate,
33 bootstrap_blobs: Vec<ExactObjectRef>,
34}
35
36impl crate::store::store_session::StoreTransaction<'_, '_> {
37 fn circle_operation_candidate(
38 self,
39 authority: &mut super::VerifiedStoreAuthority,
40 operation: &PreparedCircleOperation,
41 ) -> Result<CircleOperationCandidate, crate::DbError> {
42 circle_operation_candidate_on(
43 crate::store::store_session::StoreRecords::new(self.transaction, self.store_dir),
44 authority,
45 operation,
46 )
47 }
48}
49
50pub struct CircleOperationDiscardCandidate {
51 pub candidate: crate::BlockedMergeCandidate,
52 pub revoked_grant: Option<coven_protocol::membership::MembershipGrantId>,
53}
54
55fn circle_operation_candidate_on(
56 records: crate::store::store_session::StoreRecords<'_>,
57 authority: &mut super::VerifiedStoreAuthority,
58 operation: &PreparedCircleOperation,
59) -> Result<CircleOperationCandidate, crate::DbError> {
60 let commit_object = operation
61 .prepared_objects
62 .get("store-commit")
63 .ok_or_else(|| {
64 crate::DbError::Message("Circle operation lacks its prepared Store commit".to_string())
65 })?;
66 let head_object = operation
67 .prepared_objects
68 .get("store-head")
69 .ok_or_else(|| {
70 crate::DbError::Message("Circle operation lacks its prepared Store head".to_string())
71 })?;
72 let candidate = parse_prepared_merge_candidate_parts_on(
73 records,
74 authority,
75 &operation.commit_bytes,
76 commit_object,
77 &operation.policy.head.to_bytes(),
78 head_object,
79 )?;
80 if candidate.reference != operation.commit_ref {
81 return Err(crate::DbError::Message(
82 "Circle operation candidate differs from its durable commit reference".to_string(),
83 ));
84 }
85 let mut bootstrap_blobs = Vec::new();
86 for access in &operation.creation.access {
87 if let CircleAccessDisposition::Active {
88 bootstrap: Some(bootstrap),
89 ..
90 } = &access.leaf.value.disposition
91 {
92 for blob in &bootstrap.blobs {
93 let stored = blob.stored().ok_or_else(|| {
94 crate::DbError::Message(
95 "Circle bootstrap row blob has no exact stored locator".to_string(),
96 )
97 })?;
98 bootstrap_blobs.push(stored.object().clone());
99 }
100 }
101 }
102 Ok(CircleOperationCandidate {
103 candidate,
104 bootstrap_blobs,
105 })
106}
107
108impl StoreSession<'_> {
109 fn circle_operation_discard_candidate(
110 &mut self,
111 operation_id: String,
112 ) -> Result<CircleOperationDiscardCandidate, crate::DbError> {
113 let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
114 let journal = load_discardable_operation_on(self.conn, &operation_id)?;
115 let candidate = circle_operation_candidate_on(
116 records,
117 self.verified_store_authority,
118 journal.operation(),
119 )?;
120 let revoked_grant = match journal.state() {
121 coven_protocol::circle::CircleOperationState::Blocked {
122 block: coven_protocol::circle::CircleOperationBlock::AuthorityLost { grant_id },
123 } => Some(grant_id),
124 coven_protocol::circle::CircleOperationState::Blocked {
125 block: coven_protocol::circle::CircleOperationBlock::PositionLost { .. },
126 }
127 | coven_protocol::circle::CircleOperationState::Pending
128 | coven_protocol::circle::CircleOperationState::WaitingForCloseResponses
129 | coven_protocol::circle::CircleOperationState::Finalizing
130 | coven_protocol::circle::CircleOperationState::Discarding => None,
131 };
132 Ok(CircleOperationDiscardCandidate {
133 candidate: blocked_merge_candidate_from_prepared(candidate.candidate),
134 revoked_grant,
135 })
136 }
137
138 fn begin_circle_operation_discard(
139 &mut self,
140 root: coven_protocol::store_commit::StoreRootRef,
141 operation_id: String,
142 nonactivation: coven_protocol::remote_object::VerifiedCandidateNonactivation,
143 ) -> Result<(), crate::DbError> {
144 let nonactivation = blocked_merge_candidate_nonactivation(nonactivation)?;
145 let tx = self
146 .conn
147 .unchecked_transaction()
148 .map_err(crate::DbError::from)?;
149 let mut journal = load_discardable_operation_on(&tx, &operation_id)?;
150 let CircleOperationCandidate {
151 candidate,
152 bootstrap_blobs,
153 } = crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
154 .circle_operation_candidate(self.verified_store_authority, journal.operation())?;
155 crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
156 .begin_blocked_merge_candidate_nonactivation(
157 self.verified_store_authority,
158 &root,
159 &candidate.commit.write_id,
160 &candidate,
161 &nonactivation,
162 false,
163 &bootstrap_blobs,
164 )?;
165 journal.begin_discard().map_err(crate::DbError::from)?;
166 super::circle_controls::update_circle_operation_phase_on(&tx, &journal)?;
167 tx.commit().map_err(crate::DbError::from)
168 }
169
170 fn circle_operation_discard_terminal_verifications(
171 &mut self,
172 root: coven_protocol::store_commit::StoreRootRef,
173 operation_id: String,
174 ) -> Result<Vec<TerminalCandidateCleanupVerification>, crate::DbError> {
175 let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
176 let journal = load_discarding_operation_on(self.conn, &operation_id)?;
177 let candidate = circle_operation_candidate_on(
178 records,
179 self.verified_store_authority,
180 journal.operation(),
181 )?;
182 Ok(terminal_candidate_verification_on(
183 records,
184 self.verified_store_authority,
185 &root,
186 candidate.candidate,
187 )?
188 .into_iter()
189 .collect())
190 }
191
192 fn reconcile_circle_operation_terminal_head(
193 &mut self,
194 root: coven_protocol::store_commit::StoreRootRef,
195 operation_id: String,
196 durable: coven_protocol::remote_object::CandidateNonactivation,
197 head_nonactivation: coven_protocol::remote_object::VerifiedCandidateHeadNonactivation,
198 ) -> Result<(), crate::DbError> {
199 let tx = self
200 .conn
201 .unchecked_transaction()
202 .map_err(crate::DbError::from)?;
203 let journal = load_discarding_operation_on(&tx, &operation_id)?;
204 let store_transaction =
205 crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
206 let candidate = store_transaction
207 .circle_operation_candidate(self.verified_store_authority, journal.operation())?
208 .candidate;
209 let reference = durable.reference().map_err(crate::DbError::from)?;
210 if reference != candidate.reference {
211 return Err(crate::DbError::Message(
212 "fresh excluded-author head evidence names another candidate".to_string(),
213 ));
214 }
215 store_transaction.validate_terminal_candidate_authority(
216 self.verified_store_authority,
217 &root,
218 &candidate,
219 &durable,
220 )?;
221 let object_id = remote_object_id(&candidate.head_object);
222 let remote_exists: bool = tx
223 .query_row(
224 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
225 [object_id.to_string()],
226 |row| row.get(0),
227 )
228 .map_err(crate::DbError::from)?;
229 if !remote_exists {
230 let inert = load_protocol_inert_object_on(&tx, object_id)?;
231 if inert
232 .candidate_nonactivation_proof(&candidate.reference)
233 .map_err(crate::DbError::from)?
234 != Some(durable.proof())
235 {
236 return Err(crate::DbError::Message(
237 "protocol-inert candidate head carries another proof".to_string(),
238 ));
239 }
240 return tx.commit().map_err(crate::DbError::from);
241 }
242 let mut remote = load_remote_object_on(&tx, object_id)?;
243 let inert = remote
244 .begin_candidate_nonactivation_with_verified_head_nonactivation(
245 durable,
246 &head_nonactivation,
247 )
248 .map_err(|error| {
249 crate::DbError::context(
250 format!("reconcile excluded-author head {object_id}"),
251 error,
252 )
253 })?;
254 finish_remote_candidate_nonactivation_on(&tx, object_id, remote, inert)?;
255 tx.commit().map_err(crate::DbError::from)
256 }
257
258 fn circle_operation_discard_cleanup_targets(
259 &mut self,
260 operation_id: String,
261 ) -> Result<Vec<CandidateCleanupObject>, crate::DbError> {
262 let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
263 let journal = load_discarding_operation_on(self.conn, &operation_id)?;
264 let CircleOperationCandidate {
265 candidate,
266 bootstrap_blobs,
267 } = circle_operation_candidate_on(
268 records,
269 self.verified_store_authority,
270 journal.operation(),
271 )?;
272 merge_candidate_cleanup_targets_on(
273 self.conn,
274 &candidate.commit.write_id,
275 &candidate,
276 false,
277 &bootstrap_blobs,
278 )
279 }
280
281 fn discarding_circle_operations(&mut self) -> Result<Vec<CircleOperationId>, crate::DbError> {
282 let conn = self.conn;
283 crate::circle_operation_ids_in_phase_on(conn, |progress| {
284 matches!(
285 progress,
286 coven_protocol::circle_journal::CircleOperationProgress::Discarding
287 )
288 })?
289 .into_iter()
290 .map(|operation_id| Ok(load_discarding_operation_on(conn, &operation_id)?.operation_id))
291 .collect()
292 }
293
294 fn finish_circle_operation_discard(
295 &mut self,
296 operation_id: String,
297 ) -> Result<(), crate::DbError> {
298 let tx = self
299 .conn
300 .unchecked_transaction()
301 .map_err(crate::DbError::from)?;
302 let journal = load_discarding_operation_on(&tx, &operation_id)?;
303 let CircleOperationCandidate {
304 candidate,
305 bootstrap_blobs,
306 } = crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
307 .circle_operation_candidate(self.verified_store_authority, journal.operation())?;
308 if !merge_candidate_cleanup_targets_on(
309 &tx,
310 &candidate.commit.write_id,
311 &candidate,
312 false,
313 &bootstrap_blobs,
314 )?
315 .is_empty()
316 {
317 return Err(crate::DbError::Message(
318 "Circle operation discard still has remote cleanup targets".to_string(),
319 ));
320 }
321 let mut object_ids = candidate_graph_exact_objects(&candidate.commit)?
322 .iter()
323 .map(remote_object_id)
324 .collect::<std::collections::BTreeSet<_>>();
325 object_ids.insert(remote_object_id(&candidate.reference.object));
326 for object_id in object_ids {
327 let remote = load_remote_object_on(&tx, object_id)?;
328 if !remote
329 .candidate_cleanup_complete(&candidate.reference)
330 .map_err(|error| {
331 crate::DbError::context(
332 format!("finish Circle operation discard for {object_id}"),
333 error,
334 )
335 })?
336 {
337 return Err(crate::DbError::Message(format!(
338 "Circle discard object {object_id} is not terminal"
339 )));
340 }
341 if matches!(
342 remote,
343 coven_protocol::remote_object::RemoteObjectRecord::CandidateCommit(
344 coven_protocol::remote_object::CandidateCommitRecord {
345 state:
346 coven_protocol::remote_object::CandidateCommitState::AbsentVerified { .. },
347 ..
348 }
349 ) | coven_protocol::remote_object::RemoteObjectRecord::CandidateExclusive(
350 coven_protocol::remote_object::CandidateObjectRecord {
351 state:
352 coven_protocol::remote_object::CandidateObjectState::AbsentVerified { .. },
353 ..
354 }
355 )
356 ) && !crate::remote_object_records::delete_remote_object_on(&tx, object_id)?
357 {
358 return Err(crate::DbError::Message(format!(
359 "Circle discard object {object_id} disappeared during finalization"
360 )));
361 }
362 }
363 super::circle_controls::release_operation_payloads_on(&tx, &journal.operation_id)?;
364 let deleted = tx
365 .execute(
366 "DELETE FROM circle_operations WHERE operation_id = ?1",
367 [operation_id.as_str()],
368 )
369 .map_err(crate::DbError::from)?;
370 if deleted != 1 {
371 return Err(crate::DbError::Message(
372 "discarded Circle operation disappeared during finalization".to_string(),
373 ));
374 }
375 tx.commit().map_err(crate::DbError::from)
376 }
377}
378
379impl StoreDatabase {
380 pub async fn circle_operation_discard_candidate(
383 &self,
384 operation_id: &CircleOperationId,
385 ) -> Result<CircleOperationDiscardCandidate, crate::DbError> {
386 let operation_id = operation_id.as_str().to_string();
387 self.call_store(move |session| session.circle_operation_discard_candidate(operation_id))
388 .await
389 }
390
391 pub async fn begin_circle_operation_discard(
394 &self,
395 root: coven_protocol::store_commit::StoreRootRef,
396 operation_id: &CircleOperationId,
397 nonactivation: coven_protocol::remote_object::VerifiedCandidateNonactivation,
398 ) -> Result<(), crate::DbError> {
399 let operation_id = operation_id.as_str().to_string();
400 self.call_store(move |session| {
401 session.begin_circle_operation_discard(root, operation_id, nonactivation)
402 })
403 .await
404 }
405
406 pub async fn circle_operation_discard_terminal_verifications(
408 &self,
409 root: coven_protocol::store_commit::StoreRootRef,
410 operation_id: &CircleOperationId,
411 ) -> Result<Vec<TerminalCandidateCleanupVerification>, crate::DbError> {
412 let operation_id = operation_id.as_str().to_string();
413 self.call_store(move |session| {
414 session.circle_operation_discard_terminal_verifications(root, operation_id)
415 })
416 .await
417 }
418
419 pub async fn reconcile_circle_operation_terminal_head(
421 &self,
422 root: coven_protocol::store_commit::StoreRootRef,
423 operation_id: &CircleOperationId,
424 verified: coven_protocol::remote_object::VerifiedCandidateNonactivation,
425 ) -> Result<(), crate::DbError> {
426 if !matches!(
427 verified.proof(),
428 coven_protocol::remote_object::CandidateNonactivationProof::AuthorExclusion { .. }
429 | coven_protocol::remote_object::CandidateNonactivationProof::MergeMembershipGrantRevocation { .. }
430 ) {
431 return Err(crate::DbError::Message(
432 "terminal head reconciliation received another proof family".to_string(),
433 ));
434 }
435 let (durable, head_nonactivation) = verified
436 .into_terminal_head_nonactivation()
437 .map_err(crate::DbError::from)?;
438 let operation_id = operation_id.as_str().to_string();
439 self.call_store(move |session| {
440 session.reconcile_circle_operation_terminal_head(
441 root,
442 operation_id,
443 durable,
444 head_nonactivation,
445 )
446 })
447 .await
448 }
449
450 pub async fn circle_operation_discard_cleanup_targets(
452 &self,
453 operation_id: &CircleOperationId,
454 ) -> Result<Vec<CandidateCleanupObject>, crate::DbError> {
455 let operation_id = operation_id.as_str().to_string();
456 self.call_store(move |session| {
457 session.circle_operation_discard_cleanup_targets(operation_id)
458 })
459 .await
460 }
461
462 pub async fn discarding_circle_operations(
464 &self,
465 ) -> Result<Vec<CircleOperationId>, crate::DbError> {
466 self.call_store(|session| session.discarding_circle_operations())
467 .await
468 }
469
470 pub async fn finish_circle_operation_discard(
473 &self,
474 operation_id: &CircleOperationId,
475 ) -> Result<(), crate::DbError> {
476 let operation_id = operation_id.as_str().to_string();
477 self.call_store(move |session| session.finish_circle_operation_discard(operation_id))
478 .await
479 }
480}
481
482fn load_discardable_operation_on(
486 conn: &Connection,
487 operation_id: &str,
488) -> Result<CircleOperationJournal, crate::DbError> {
489 let journal = crate::load_circle_operation_on(conn, operation_id)?.ok_or_else(|| {
490 crate::DbError::Message(format!("circle operation {operation_id} is absent"))
491 })?;
492 if journal.is_discarding() {
493 return Err(crate::DbError::Message(format!(
494 "circle operation {operation_id} is already discarding"
495 )));
496 }
497 Ok(journal)
498}
499
500fn load_discarding_operation_on(
501 conn: &Connection,
502 operation_id: &str,
503) -> Result<CircleOperationJournal, crate::DbError> {
504 let journal = crate::load_circle_operation_on(conn, operation_id)?.ok_or_else(|| {
505 crate::DbError::Message(format!("circle operation {operation_id} is absent"))
506 })?;
507 if !journal.is_discarding() {
508 return Err(crate::DbError::Message(format!(
509 "circle operation {operation_id} is not discarding"
510 )));
511 }
512 Ok(journal)
513}