1use crate::*;
2use coven_protocol::objects::ExactObjectRef;
3use coven_protocol::remote_object::{remote_object_id, RemoteObjectRecord};
4use coven_protocol::store_commit::{
5 ObjectHash, StoreBatchCommit, StoreBatchCommitRef, StoreCommitCoord, StoreDeviceHead,
6 StoreDeviceRegistrationRef, StoreHistoryCut, VerifiedStoreBatchCommit,
7};
8use coven_protocol::write::WriteId;
9use rusqlite::Connection;
10use std::collections::{BTreeMap, BTreeSet};
11
12use super::publication_state::PreparedStoreWriteState;
13#[derive(Debug, Clone)]
14pub struct PreparedMergeCandidate {
15 pub commit: VerifiedStoreBatchCommit,
16 pub reference: StoreBatchCommitRef,
17 pub canonical_signed_bytes: Vec<u8>,
18 pub head: StoreDeviceHead,
19 pub head_object: ExactObjectRef,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct CandidateCleanupObject {
24 pub object: ExactObjectRef,
25}
26
27pub(crate) fn begin_candidate_nonactivation_targets_on(
35 tx: &rusqlite::Transaction<'_>,
36 candidate: &StoreBatchCommitRef,
37 objects: &[ExactObjectRef],
38 nonactivation: &coven_protocol::remote_object::CandidateNonactivation,
39) -> Result<Vec<CandidateCleanupObject>, DbError> {
40 let mut unique = BTreeSet::new();
41 let mut cleanup = Vec::new();
42 for object in objects {
43 let object_id = remote_object_id(object);
44 if !unique.insert(object_id) {
45 return Err(DbError::Message(
46 "losing candidate repeats an exact owned object".to_string(),
47 ));
48 }
49 if let Some(target) =
50 begin_remote_candidate_nonactivation_on(tx, object_id, nonactivation.clone())?
51 {
52 cleanup.push(CandidateCleanupObject { object: target });
53 }
54 }
55 if !objects.contains(&candidate.object)
56 || !cleanup
57 .iter()
58 .any(|target| target.object == candidate.object)
59 {
60 return Err(DbError::Message(
61 "losing candidate has no exact commit cleanup target".to_string(),
62 ));
63 }
64 cleanup.sort_by(|left, right| left.object.cmp(&right.object));
65 Ok(cleanup)
66}
67
68pub(crate) fn candidate_cleanup_targets_on(
73 conn: &Connection,
74 candidate: &StoreBatchCommitRef,
75 objects: &[ExactObjectRef],
76) -> Result<Vec<CandidateCleanupObject>, DbError> {
77 let mut unique = BTreeSet::new();
78 let mut cleanup = Vec::new();
79 for object in objects {
80 let object_id = remote_object_id(object);
81 if !unique.insert(object_id) {
82 return Err(DbError::Message(
83 "candidate cleanup repeats an exact object".to_string(),
84 ));
85 }
86 let remote = load_remote_object_on(conn, object_id)?;
87 if let Some(target) = remote.cleanup_target() {
88 cleanup.push(CandidateCleanupObject {
89 object: target.clone(),
90 });
91 } else if !remote
92 .candidate_cleanup_complete(candidate)
93 .map_err(DbError::from)?
94 {
95 return Err(DbError::Message(format!(
96 "candidate object {object_id} has no cleanup decision"
97 )));
98 }
99 }
100 cleanup.sort_by(|left, right| left.object.cmp(&right.object));
101 Ok(cleanup)
102}
103
104pub(crate) fn require_candidate_cleanup_complete_on(
105 conn: &Connection,
106 candidate: &StoreBatchCommitRef,
107 objects: &[ExactObjectRef],
108 context: &str,
109) -> Result<(), DbError> {
110 if candidate_cleanup_targets_on(conn, candidate, objects)?.is_empty() {
111 Ok(())
112 } else {
113 Err(DbError::Message(context.to_string()))
114 }
115}
116
117pub(crate) fn delete_remote_objects_on(
118 tx: &rusqlite::Transaction<'_>,
119 object_ids: impl IntoIterator<Item = ObjectHash>,
120 context: &str,
121) -> Result<(), DbError> {
122 let mut unique = BTreeSet::new();
123 for object_id in object_ids {
124 if !unique.insert(object_id) {
125 return Err(DbError::Message(format!(
126 "{context} repeats remote object {object_id}"
127 )));
128 }
129 if !crate::remote_object_records::delete_remote_object_on(tx, object_id)? {
130 return Err(DbError::Message(format!(
131 "{context} object {object_id} disappeared during cleanup"
132 )));
133 }
134 }
135 Ok(())
136}
137
138pub(super) fn parse_prepared_merge_candidate_on(
139 records: crate::store::store_session::StoreRecords<'_>,
140 authority: &mut super::verified_store_authority::VerifiedStoreAuthority,
141 prepared: &PreparedStoreWriteState,
142) -> Result<PreparedMergeCandidate, DbError> {
143 authority.prepared_merge_candidate_on(records, prepared)
144}
145
146pub(super) fn prepared_merge_candidate_objects(
147 prepared: &PreparedStoreWriteState,
148) -> (
149 &crate::DurablePreparedProtocolObject,
150 &crate::DurablePreparedProtocolObject,
151) {
152 match prepared {
153 PreparedStoreWriteState::Publication { commit, head, .. } => (commit, head),
154 PreparedStoreWriteState::MergeAbandonment {
155 candidate_commit,
156 candidate_head,
157 ..
158 } => (candidate_commit, candidate_head),
159 }
160}
161
162pub(super) fn parse_prepared_merge_candidate_parts_on(
168 records: crate::store::store_session::StoreRecords<'_>,
169 authority: &mut super::verified_store_authority::VerifiedStoreAuthority,
170 commit_bytes: &[u8],
171 commit_object: &ExactObjectRef,
172 head_bytes: &[u8],
173 head_object: &ExactObjectRef,
174) -> Result<PreparedMergeCandidate, DbError> {
175 authority.prepared_merge_candidate_parts_on(
176 records,
177 commit_bytes,
178 commit_object,
179 head_bytes,
180 head_object,
181 )
182}
183
184#[allow(clippy::too_many_arguments)]
185pub(super) fn verify_prepared_merge_candidate_parts(
186 root: &coven_protocol::store_commit::StoreRootRef,
187 unverified: StoreBatchCommit,
188 registration: &coven_protocol::store_commit::StoreDeviceRegistration,
189 commit_bytes: &[u8],
190 commit_object: &ExactObjectRef,
191 head_bytes: &[u8],
192 head_object: &ExactObjectRef,
193) -> Result<PreparedMergeCandidate, DbError> {
194 let coord = StoreCommitCoord {
195 stream_id: coven_protocol::store_commit::StreamActivation::device_authorized_stream_id(
196 root.store_root_hash,
197 &unverified.author_registration,
198 coven_protocol::store_commit::StreamAnchorDomain::StoreAnnouncements,
199 ),
200 sequence: unverified.seq(),
201 };
202 let value = VerifiedStoreBatchCommit::parse_prepared(
203 commit_bytes,
204 root.store_root_hash,
205 coord,
206 commit_object.clone(),
207 registration,
208 )
209 .map_err(|error| DbError::context("verify Merge candidate", error))?;
210 let reference = value.reference().clone();
211 let head_value =
212 StoreDeviceHead::parse_at(head_bytes, root.store_root_hash, registration, &reference)
213 .map_err(|error| DbError::context("verify Merge candidate head", error))?;
214 Ok(PreparedMergeCandidate {
215 commit: value,
216 reference,
217 canonical_signed_bytes: commit_bytes.to_vec(),
218 head: head_value,
219 head_object: head_object.clone(),
220 })
221}
222
223pub fn blocked_merge_candidate_from_prepared(
224 candidate: PreparedMergeCandidate,
225) -> BlockedMergeCandidate {
226 BlockedMergeCandidate {
227 commit: candidate.commit,
228 commit_bytes: candidate.canonical_signed_bytes,
229 commit_object: candidate.reference.object,
230 head: candidate.head,
231 head_object: candidate.head_object,
232 }
233}
234
235pub(super) fn parse_prepared_merge_publication_on(
236 records: crate::store::store_session::StoreRecords<'_>,
237 authority: &mut super::verified_store_authority::VerifiedStoreAuthority,
238 prepared: &PreparedStoreWriteState,
239) -> Result<PreparedMergeCandidate, DbError> {
240 match prepared {
241 PreparedStoreWriteState::Publication { commit, head, .. } => {
242 parse_prepared_merge_candidate_parts_on(
243 records,
244 authority,
245 commit.semantic_bytes(),
246 commit.prepared().reference(),
247 head.semantic_bytes(),
248 head.prepared().reference(),
249 )
250 }
251 PreparedStoreWriteState::MergeAbandonment {
252 authority_commit,
253 authority_head,
254 ..
255 } => parse_prepared_merge_candidate_parts_on(
256 records,
257 authority,
258 authority_commit.semantic_bytes(),
259 authority_commit.prepared().reference(),
260 authority_head.semantic_bytes(),
261 authority_head.prepared().reference(),
262 ),
263 }
264}
265
266pub enum MergeCandidateHeadEvidence<'a> {
267 OccupiedByProof,
268 Verified(&'a coven_protocol::remote_object::VerifiedCandidateHeadNonactivation),
269}
270
271pub(super) fn author_exclusion_activation_for_candidate_on(
272 records: crate::store::store_session::StoreRecords<'_>,
273 retained: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
274 root: &coven_protocol::store_commit::StoreRootRef,
275 candidate: &StoreBatchCommitRef,
276 author: &StoreDeviceRegistrationRef,
277) -> Result<Option<AuthorExclusionActivationLocator>, DbError> {
278 let expected_stream =
279 coven_protocol::store_commit::StreamActivation::device_authorized_stream_id(
280 root.store_root_hash,
281 author,
282 coven_protocol::store_commit::StreamAnchorDomain::StoreAnnouncements,
283 );
284 let StoreCommitCoord {
285 stream_id,
286 sequence,
287 } = &candidate.coord;
288 if *stream_id != expected_stream {
289 return Err(DbError::Message(
290 "candidate stream differs from its exact author registration".to_string(),
291 ));
292 }
293 let state = records.current_store_device_state()?;
294 let Some(record) = state.devices.get(&author.device_id) else {
295 return Err(DbError::Message(
296 "candidate author is absent from the current device state".to_string(),
297 ));
298 };
299 if record.registration != *author {
300 return Err(DbError::Message(
301 "candidate author differs from the current device registration".to_string(),
302 ));
303 }
304 let coven_protocol::store_commit::StoreDeviceStatus::Inactive {
305 terminals,
306 accepted_cut: _,
307 } = &record.status
308 else {
309 return Ok(None);
310 };
311 select_author_exclusion_activation_locator(
312 terminals.as_slice(),
313 &expected_stream,
314 *sequence,
315 |exclusion| load_author_exclusion_activation_locator_on(records, retained, root, exclusion),
316 )
317}
318
319pub fn select_author_exclusion_activation_locator(
320 terminals: &[coven_protocol::store_commit::StoreDeviceExclusionRef],
321 expected_stream: &coven_protocol::causal_grants::AuthorStreamId,
322 sequence: u64,
323 mut load: impl FnMut(
324 &coven_protocol::store_commit::StoreDeviceExclusionRef,
325 ) -> Result<AuthorExclusionActivationLocator, DbError>,
326) -> Result<Option<AuthorExclusionActivationLocator>, DbError> {
327 for exclusion in terminals {
328 let locator = load(exclusion)?;
329 let excluded_by_this_terminal = match locator.accepted_cut().get(expected_stream) {
330 Some(reference) => sequence > reference.coord.sequence(),
331 None => true,
332 };
333 if excluded_by_this_terminal {
334 return Ok(Some(locator));
335 }
336 }
337 Ok(None)
338}
339
340pub(super) fn load_author_exclusion_activation_locator_on(
341 records: crate::store::store_session::StoreRecords<'_>,
342 retained: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
343 root: &coven_protocol::store_commit::StoreRootRef,
344 exclusion: &coven_protocol::store_commit::StoreDeviceExclusionRef,
345) -> Result<AuthorExclusionActivationLocator, DbError> {
346 let exclusion_json = serde_json::to_string(exclusion)
347 .map_err(|error| DbError::context("serialize author exclusion reference", error))?;
348 let stored = records.author_exclusion_activation_row(&exclusion_json)?;
349 let Some((accepted_cut, activation_commit, activation_head)) = stored else {
350 return Err(DbError::Message(
351 "applied author exclusion has no exact activation locator".to_string(),
352 ));
353 };
354 let accepted_cut = serde_json::from_str(&accepted_cut)
355 .map_err(|error| DbError::context("parse author exclusion accepted cut", error))?;
356 let activation_commit = serde_json::from_str(&activation_commit)
357 .map_err(|error| DbError::context("parse author exclusion activation commit", error))?;
358 let activation_head = serde_json::from_str(&activation_head)
359 .map_err(|error| DbError::context("parse author exclusion activation head", error))?;
360 let locator = AuthorExclusionActivationLocator::verified(
361 exclusion.clone(),
362 accepted_cut,
363 activation_commit,
364 activation_head,
365 );
366 let retained =
367 retained.retained_materialization_by_ref_on(records, locator.activation_commit())?;
368 if retained.root() != root {
369 return Err(DbError::Message(
370 "author exclusion activation belongs to another Store root".to_string(),
371 ));
372 }
373 let accepted_cut = StoreHistoryCut(locator.accepted_cut().clone());
374 if retained.activation_head_object() != &locator.activation_head().object
375 || retained.activation_head().head_hash() != locator.activation_head().head_hash
376 || !retained
377 .device_operations()
378 .exclusions()
379 .any(|(candidate, cut)| candidate == exclusion && cut == &accepted_cut)
380 {
381 return Err(DbError::Message(
382 "author exclusion locator differs from its exact retained activation".to_string(),
383 ));
384 }
385 Ok(locator)
386}
387
388pub enum BlockedMergeCandidateNonactivation {
389 Merge(coven_protocol::remote_object::CandidateNonactivation),
390 Terminal {
391 durable: coven_protocol::remote_object::CandidateNonactivation,
392 head_nonactivation: coven_protocol::remote_object::VerifiedCandidateHeadNonactivation,
393 },
394}
395
396pub fn blocked_merge_candidate_nonactivation(
397 verified: coven_protocol::remote_object::VerifiedCandidateNonactivation,
398) -> Result<BlockedMergeCandidateNonactivation, DbError> {
399 if matches!(
400 verified.proof(),
401 coven_protocol::remote_object::CandidateNonactivationProof::AuthorExclusion { .. }
402 | coven_protocol::remote_object::CandidateNonactivationProof::MergeMembershipGrantRevocation { .. }
403 | coven_protocol::remote_object::CandidateNonactivationProof::MergeDependencyRetraction { .. }
404 ) {
405 let (durable, head_nonactivation) = verified
406 .into_terminal_head_nonactivation()
407 .map_err(DbError::from)?;
408 return Ok(BlockedMergeCandidateNonactivation::Terminal {
409 durable,
410 head_nonactivation,
411 });
412 }
413 verified.merge_winner_commit().map_err(DbError::from)?;
414 Ok(BlockedMergeCandidateNonactivation::Merge(
415 verified.into_durable(),
416 ))
417}
418
419pub(super) fn validate_terminal_candidate_authority_on(
420 records: crate::store::store_session::StoreRecords<'_>,
421 retained: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
422 root: &coven_protocol::store_commit::StoreRootRef,
423 candidate: &PreparedMergeCandidate,
424 durable: &coven_protocol::remote_object::CandidateNonactivation,
425) -> Result<(), DbError> {
426 if durable.reference().map_err(DbError::from)? != candidate.reference {
427 return Err(DbError::Message(
428 "terminal candidate authority names another candidate".to_string(),
429 ));
430 }
431 validate_terminal_nonactivation_authority_on(records, retained, root, durable)
432}
433
434pub(super) fn validate_terminal_nonactivation_authority_on(
435 records: crate::store::store_session::StoreRecords<'_>,
436 retained: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
437 root: &coven_protocol::store_commit::StoreRootRef,
438 durable: &coven_protocol::remote_object::CandidateNonactivation,
439) -> Result<(), DbError> {
440 match durable.proof() {
441 coven_protocol::remote_object::CandidateNonactivationProof::AuthorExclusion {
442 exclusion,
443 accepted_cut,
444 activation_head,
445 } => {
446 let commit: StoreBatchCommit = serde_json::from_slice(
447 &durable.candidate().canonical_signed_bytes,
448 )
449 .map_err(|error| DbError::context("terminal candidate commit", error))?;
450 let reference = durable
451 .reference()
452 .map_err(DbError::from)?;
453 let current = author_exclusion_activation_for_candidate_on(
454 records,
455 retained,
456 root,
457 &reference,
458 &commit.author_registration,
459 )?
460 .ok_or_else(|| {
461 DbError::Message(
462 "candidate is no longer excluded by the selected terminal cutoff".to_string(),
463 )
464 })?;
465 if current.exclusion() != exclusion
466 || current.accepted_cut() != accepted_cut
467 || current.activation_head() != activation_head
468 {
469 return Err(DbError::Message(
470 "author-exclusion activation changed after remote verification".to_string(),
471 ));
472 }
473 }
474 coven_protocol::remote_object::CandidateNonactivationProof::MergeMembershipGrantRevocation {
475 activation_commit,
476 ..
477 } => {
478 let StoreCommitCoord {
479 stream_id,
480 sequence,
481 } = &activation_commit.coord;
482 if records
483 .materialized_commit_ref(&stream_id.to_string(), *sequence)?
484 .as_ref()
485 != Some(activation_commit)
486 {
487 return Err(DbError::Message(
488 "membership-grant revocation activation is no longer current accepted history"
489 .to_string(),
490 ));
491 }
492 }
493 coven_protocol::remote_object::CandidateNonactivationProof::MergeDependencyRetraction {
494 dependency_nonactivation,
495 ..
496 } => {
497 validate_terminal_nonactivation_authority_on(
498 records,
499 retained,
500 root,
501 dependency_nonactivation,
502 )?;
503 }
504 coven_protocol::remote_object::CandidateNonactivationProof::MergeWinner { .. } => {
505 return Err(DbError::Message(
506 "terminal candidate authority received another proof family".to_string(),
507 ));
508 }
509 }
510 Ok(())
511}
512
513pub(crate) fn begin_merge_candidate_nonactivation_on(
514 conn: &rusqlite::Transaction<'_>,
515 write_id: &WriteId,
516 candidate: &PreparedMergeCandidate,
517 nonactivation: &coven_protocol::remote_object::CandidateNonactivation,
518 include_indexed_blobs: bool,
519 extra_objects: &[ExactObjectRef],
520) -> Result<(), DbError> {
521 begin_merge_candidate_nonactivation_with_head_evidence_on(
522 conn,
523 write_id,
524 candidate,
525 nonactivation,
526 include_indexed_blobs,
527 extra_objects,
528 MergeCandidateHeadEvidence::OccupiedByProof,
529 )
530}
531
532pub(crate) fn begin_merge_candidate_nonactivation_with_verified_head_on(
533 conn: &rusqlite::Transaction<'_>,
534 write_id: &WriteId,
535 candidate: &PreparedMergeCandidate,
536 nonactivation: &coven_protocol::remote_object::CandidateNonactivation,
537 include_indexed_blobs: bool,
538 extra_objects: &[ExactObjectRef],
539 head_nonactivation: &coven_protocol::remote_object::VerifiedCandidateHeadNonactivation,
540) -> Result<(), DbError> {
541 begin_merge_candidate_nonactivation_with_head_evidence_on(
542 conn,
543 write_id,
544 candidate,
545 nonactivation,
546 include_indexed_blobs,
547 extra_objects,
548 MergeCandidateHeadEvidence::Verified(head_nonactivation),
549 )
550}
551
552pub(crate) fn begin_merge_candidate_nonactivation_with_head_evidence_on(
553 conn: &rusqlite::Transaction<'_>,
554 write_id: &WriteId,
555 candidate: &PreparedMergeCandidate,
556 nonactivation: &coven_protocol::remote_object::CandidateNonactivation,
557 include_indexed_blobs: bool,
558 extra_objects: &[ExactObjectRef],
559 head_evidence: MergeCandidateHeadEvidence<'_>,
560) -> Result<(), DbError> {
561 if nonactivation.reference().map_err(DbError::from)? != candidate.reference
562 || nonactivation.candidate().canonical_signed_bytes != candidate.canonical_signed_bytes
563 {
564 return Err(DbError::Message(
565 "verified Merge nonactivation names another prepared candidate".to_string(),
566 ));
567 }
568 let mut object_ids = candidate_graph_exact_objects(&candidate.commit)?
569 .iter()
570 .map(|object| remote_object_id(object).to_string())
571 .collect::<Vec<_>>();
572 if include_indexed_blobs {
573 let mut statement = conn
574 .prepare(
575 "SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1
576 ORDER BY remote_object_id",
577 )
578 .map_err(DbError::from)?;
579 let indexed = statement
580 .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
581 .map_err(DbError::from)?
582 .collect::<Result<Vec<_>, _>>()
583 .map_err(DbError::from)?;
584 drop(statement);
585 object_ids.extend(indexed);
586 }
587 object_ids.extend(
588 extra_objects
589 .iter()
590 .map(|object| remote_object_id(object).to_string()),
591 );
592 for encoded in object_ids {
593 let object_id: ObjectHash = encoded
594 .parse()
595 .map_err(|error| DbError::context("Merge conflict remote object id", error))?;
596 let _cleanup_target =
597 begin_remote_candidate_nonactivation_on(conn, object_id, nonactivation.clone())?;
598 }
599 let head_object_id = remote_object_id(&candidate.head_object);
600 let _head_cleanup_target = match head_evidence {
601 MergeCandidateHeadEvidence::OccupiedByProof => {
602 begin_remote_candidate_nonactivation_on(conn, head_object_id, nonactivation.clone())?
603 }
604 MergeCandidateHeadEvidence::Verified(head_nonactivation) => {
605 begin_remote_candidate_nonactivation_with_verified_head_on(
606 conn,
607 head_object_id,
608 nonactivation.clone(),
609 head_nonactivation,
610 )?
611 }
612 };
613 let commit_object_id = remote_object_id(&candidate.reference.object);
614 let _cleanup_target =
615 begin_remote_candidate_nonactivation_on(conn, commit_object_id, nonactivation.clone())?;
616 Ok(())
617}
618
619pub(super) fn terminal_candidate_verification_on(
625 records: crate::store::store_session::StoreRecords<'_>,
626 retained: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
627 root: &coven_protocol::store_commit::StoreRootRef,
628 candidate: PreparedMergeCandidate,
629) -> Result<Option<TerminalCandidateCleanupVerification>, DbError> {
630 let remote = records.remote_object(remote_object_id(&candidate.reference.object))?;
631 let Some(proof) = remote
632 .candidate_nonactivation_proof(&candidate.reference)
633 .map_err(DbError::from)?
634 else {
635 return Ok(None);
636 };
637 let authority = match proof {
638 coven_protocol::remote_object::CandidateNonactivationProof::AuthorExclusion {
639 exclusion,
640 ..
641 } => TerminalCandidateAuthority::AuthorExclusion(
642 load_author_exclusion_activation_locator_on(records, retained, root, exclusion)?,
643 ),
644 coven_protocol::remote_object::CandidateNonactivationProof::MergeMembershipGrantRevocation {
645 grant_id,
646 membership,
647 activation_commit,
648 activation_head,
649 } => TerminalCandidateAuthority::MembershipGrantRevocation {
650 grant_id: grant_id.clone(),
651 membership: membership.clone(),
652 activation_commit: activation_commit.clone(),
653 activation_head: activation_head.clone(),
654 },
655 coven_protocol::remote_object::CandidateNonactivationProof::MergeDependencyRetraction { .. } => {
656 let durable = coven_protocol::remote_object::CandidateNonactivation::from_durable_parts(
657 &candidate.reference,
658 &candidate.commit,
659 proof.clone(),
660 )
661 .map_err(DbError::from)?;
662 validate_terminal_nonactivation_authority_on(records, retained, root, &durable)?;
663 TerminalCandidateAuthority::DependencyRetraction(
664 coven_protocol::remote_object::VerifiedDependencyRetractionAuthority::after_live_authority_check(durable)
665 .map_err(DbError::from)?,
666 )
667 }
668 coven_protocol::remote_object::CandidateNonactivationProof::MergeWinner { .. } => {
669 return Ok(None)
670 }
671 };
672 Ok(Some(TerminalCandidateCleanupVerification {
673 authority,
674 candidate: blocked_merge_candidate_from_prepared(candidate),
675 }))
676}
677
678pub(crate) fn merge_candidate_cleanup_targets_on(
679 conn: &Connection,
680 write_id: &WriteId,
681 candidate: &PreparedMergeCandidate,
682 include_indexed_blobs: bool,
683 extra_objects: &[ExactObjectRef],
684) -> Result<Vec<CandidateCleanupObject>, DbError> {
685 let commit_remote = load_remote_object_on(conn, remote_object_id(&candidate.reference.object))?;
686 if !matches!(
687 &commit_remote,
688 RemoteObjectRecord::CandidateCommit(record)
689 if matches!(
690 &record.state,
691 coven_protocol::remote_object::CandidateCommitState::CleanupPending {
692 proof: coven_protocol::remote_object::CandidateNonactivationProof::MergeWinner { .. }
693 | coven_protocol::remote_object::CandidateNonactivationProof::AuthorExclusion { .. }
694 | coven_protocol::remote_object::CandidateNonactivationProof::MergeMembershipGrantRevocation { .. }
695 } | coven_protocol::remote_object::CandidateCommitState::AbsentVerified {
696 proof: coven_protocol::remote_object::CandidateNonactivationProof::MergeWinner { .. }
697 | coven_protocol::remote_object::CandidateNonactivationProof::AuthorExclusion { .. }
698 | coven_protocol::remote_object::CandidateNonactivationProof::MergeMembershipGrantRevocation { .. }
699 }
700 )
701 ) {
702 return Err(DbError::Message(
703 "Merge candidate has no durable nonactivation proof".to_string(),
704 ));
705 }
706 let mut cleanup = BTreeMap::new();
707 {
708 let mut encoded = candidate_graph_exact_objects(&candidate.commit)?
709 .iter()
710 .map(|object| remote_object_id(object).to_string())
711 .collect::<Vec<_>>();
712 if include_indexed_blobs {
713 let mut statement = conn
714 .prepare("SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1")
715 .map_err(DbError::from)?;
716 let indexed = statement
717 .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
718 .map_err(DbError::from)?
719 .collect::<Result<Vec<_>, _>>()
720 .map_err(DbError::from)?;
721 drop(statement);
722 encoded.extend(indexed);
723 }
724 encoded.extend(
725 extra_objects
726 .iter()
727 .map(|object| remote_object_id(object).to_string()),
728 );
729 for encoded in encoded {
730 let object_id: ObjectHash = encoded
731 .parse()
732 .map_err(|error| DbError::context("Merge cleanup remote object id", error))?;
733 let remote = load_remote_object_on(conn, object_id)?;
734 if let Some(object) = remote.cleanup_target() {
735 cleanup.insert(
736 object.clone(),
737 CandidateCleanupObject {
738 object: object.clone(),
739 },
740 );
741 } else if !remote
742 .candidate_cleanup_complete(&candidate.reference)
743 .map_err(|error| DbError::context(format!("Merge cleanup {object_id}"), error))?
744 {
745 return Err(DbError::Message(format!(
746 "Merge candidate object {object_id} has no cleanup transition"
747 )));
748 }
749 }
750 }
751 let head_cleanup =
752 load_merge_candidate_head_cleanup_on(conn, &candidate.head_object, &candidate.reference)?;
753 if matches!(
754 head_cleanup,
755 MergeCandidateHeadCleanup::Remote { complete: false }
756 ) {
757 return Err(DbError::Message(
758 "Merge candidate head absence is not verified".to_string(),
759 ));
760 }
761 let mut targets = Vec::new();
762 for object in candidate_graph_exact_objects(&candidate.commit)?
763 .into_iter()
764 .chain(extra_objects.iter().cloned())
765 {
766 if let Some(target) = cleanup.remove(&object) {
767 targets.push(target);
768 }
769 }
770 if !cleanup.is_empty() {
771 return Err(DbError::Message(
772 "Merge cleanup contains an object outside the signed candidate manifest".to_string(),
773 ));
774 }
775 if let Some(object) = commit_remote.cleanup_target() {
776 targets.push(CandidateCleanupObject {
777 object: object.clone(),
778 });
779 } else if !commit_remote
780 .candidate_cleanup_complete(&candidate.reference)
781 .map_err(|error| DbError::context("Merge cleanup commit", error))?
782 {
783 return Err(DbError::Message(
784 "Merge candidate commit cleanup is incomplete".to_string(),
785 ));
786 }
787 Ok(targets)
788}
789
790pub(crate) fn finish_merge_retraction_cleanup_on(
791 tx: &rusqlite::Transaction<'_>,
792 candidate: &PreparedMergeCandidate,
793) -> Result<(), DbError> {
794 if !merge_candidate_cleanup_targets_on(tx, &candidate.commit.write_id, candidate, false, &[])?
795 .is_empty()
796 {
797 return Err(DbError::Message(
798 "Merge retraction cleanup still has remote targets".to_string(),
799 ));
800 }
801 let mut object_ids = candidate_graph_exact_objects(&candidate.commit)?
802 .iter()
803 .map(remote_object_id)
804 .collect::<BTreeSet<_>>();
805 object_ids.insert(remote_object_id(&candidate.reference.object));
806 for object_id in object_ids {
807 let remote = load_remote_object_on(tx, object_id)?;
808 if !remote
809 .candidate_cleanup_complete(&candidate.reference)
810 .map_err(|error| {
811 DbError::context(
812 format!("finish Merge retraction cleanup for {object_id}"),
813 error,
814 )
815 })?
816 {
817 return Err(DbError::Message(format!(
818 "Merge retraction object {object_id} is not terminal"
819 )));
820 }
821 if matches!(
822 remote,
823 RemoteObjectRecord::CandidateCommit(
824 coven_protocol::remote_object::CandidateCommitRecord {
825 state: coven_protocol::remote_object::CandidateCommitState::AbsentVerified { .. },
826 ..
827 }
828 ) | RemoteObjectRecord::CandidateExclusive(
829 coven_protocol::remote_object::CandidateObjectRecord {
830 state: coven_protocol::remote_object::CandidateObjectState::AbsentVerified { .. },
831 ..
832 }
833 )
834 ) && !crate::remote_object_records::delete_remote_object_on(tx, object_id)?
835 {
836 return Err(DbError::Message(format!(
837 "Merge retraction object {object_id} disappeared during finalization"
838 )));
839 }
840 }
841 let StoreCommitCoord {
842 stream_id,
843 sequence,
844 } = &candidate.reference.coord;
845 let deleted = tx
846 .execute(
847 "DELETE FROM merge_retraction_cleanups
848 WHERE device_id = ?1 AND seq = ?2 AND commit_ref = ?3",
849 rusqlite::params![
850 stream_id.to_string(),
851 Database::sequence_to_sqlite(&stream_id.to_string(), *sequence)?,
852 serde_json::to_string(&candidate.reference).map_err(|error| {
853 DbError::context("serialize completed Merge retraction cleanup ref", error)
854 })?,
855 ],
856 )
857 .map_err(DbError::from)?;
858 if deleted != 1 {
859 return Err(DbError::Message(
860 "Merge retraction cleanup disappeared during finalization".to_string(),
861 ));
862 }
863 Ok(())
864}
865
866pub enum MergeCandidateHeadCleanup {
867 Remote { complete: bool },
868 ProtocolInert,
869}
870
871pub(crate) fn load_merge_candidate_head_cleanup_on(
872 conn: &Connection,
873 head: &ExactObjectRef,
874 candidate: &StoreBatchCommitRef,
875) -> Result<MergeCandidateHeadCleanup, DbError> {
876 let object_id = remote_object_id(head);
877 let (remote_exists, inert_exists): (bool, bool) = conn
878 .query_row(
879 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1),
880 EXISTS(SELECT 1 FROM protocol_inert_objects WHERE object_id = ?1)",
881 [object_id.to_string()],
882 |row| Ok((row.get(0)?, row.get(1)?)),
883 )
884 .map_err(DbError::from)?;
885 match (remote_exists, inert_exists) {
886 (true, false) => load_remote_object_on(conn, object_id)?
887 .candidate_cleanup_complete(candidate)
888 .map(|complete| MergeCandidateHeadCleanup::Remote { complete })
889 .map_err(|error| DbError::context("Merge cleanup head", error)),
890 (false, true) => {
891 let inert = load_protocol_inert_object_on(conn, object_id)?;
892 if !inert
893 .is_terminal_head_for(candidate, head)
894 .map_err(|error| DbError::context("Merge cleanup inert head", error))?
895 {
896 return Err(DbError::Message(format!(
897 "protocol-inert Merge head {object_id} does not prove this excluded candidate"
898 )));
899 }
900 Ok(MergeCandidateHeadCleanup::ProtocolInert)
901 }
902 (false, false) => Err(DbError::Message(format!(
903 "Merge candidate head {object_id} is absent from durable remote state"
904 ))),
905 (true, true) => Err(DbError::Message(format!(
906 "Merge candidate head {object_id} is both active and protocol-inert"
907 ))),
908 }
909}
910
911pub(crate) fn remove_cleaned_merge_authority_on(
912 tx: &rusqlite::Transaction<'_>,
913 authority: &PreparedMergeCandidate,
914) -> Result<(), DbError> {
915 for object in [
916 authority.reference.object.clone(),
917 authority.head_object.clone(),
918 ] {
919 let object_id = remote_object_id(&object);
920 let remote = load_remote_object_on(tx, object_id)?;
921 if !remote
922 .candidate_cleanup_complete(&authority.reference)
923 .map_err(|error| {
924 DbError::context(
925 format!("validate abandoned authority cleanup for {object_id}"),
926 error,
927 )
928 })?
929 {
930 return Err(DbError::Message(
931 "losing Merge abandonment cleanup is incomplete".to_string(),
932 ));
933 }
934 if !crate::remote_object_records::delete_remote_object_on(tx, object_id)? {
935 return Err(DbError::Message(format!(
936 "abandoned authority object {object_id} disappeared during removal"
937 )));
938 }
939 }
940 Ok(())
941}
942
943pub(crate) fn remove_cleaned_author_excluded_merge_authority_on(
944 tx: &rusqlite::Transaction<'_>,
945 authority: &PreparedMergeCandidate,
946) -> Result<(), DbError> {
947 let commit_object_id = remote_object_id(&authority.reference.object);
948 let commit = load_remote_object_on(tx, commit_object_id)?;
949 if !commit
950 .candidate_cleanup_complete(&authority.reference)
951 .map_err(|error| {
952 DbError::context(
953 format!("validate excluded abandonment commit cleanup for {commit_object_id}"),
954 error,
955 )
956 })?
957 {
958 return Err(DbError::Message(
959 "excluded Merge abandonment commit cleanup is incomplete".to_string(),
960 ));
961 }
962 if !crate::remote_object_records::delete_remote_object_on(tx, commit_object_id)? {
963 return Err(DbError::Message(
964 "excluded Merge abandonment commit disappeared during removal".to_string(),
965 ));
966 }
967
968 let head = &authority.head_object;
969 if let MergeCandidateHeadCleanup::Remote { complete } =
970 load_merge_candidate_head_cleanup_on(tx, head, &authority.reference)?
971 {
972 if !complete {
973 return Err(DbError::Message(
974 "excluded Merge abandonment head cleanup is incomplete".to_string(),
975 ));
976 }
977 let head_object_id = remote_object_id(head);
978 if !crate::remote_object_records::delete_remote_object_on(tx, head_object_id)? {
979 return Err(DbError::Message(
980 "excluded Merge abandonment head disappeared during removal".to_string(),
981 ));
982 }
983 }
984 Ok(())
985}