1use coven_foundation::store_dir::StoreDir;
2use coven_protocol::remote_object::{ClosedRemoteObject, SemanticPayload};
3
4use crate::blob_records::remote_audience_to_db;
5use crate::store_reclaim_records::store_reclaim_journal_error;
6
7use super::*;
8
9pub fn candidate_graph_exact_objects(
10 commit: &StoreBatchCommit,
11) -> Result<Vec<ExactObjectRef>, DbError> {
12 coven_protocol::remote_object::CandidateObjectGraph::from_commit(commit)
13 .map(|graph| graph.exact_objects().cloned().collect())
14 .map_err(|error| DbError::context("closed candidate object graph", error))
15}
16
17pub(crate) fn validate_remote_object_on(
24 conn: &Connection,
25 object_id: ObjectHash,
26 expected_object: &ExactObjectRef,
27 expected_semantic_bytes: &[u8],
28) -> Result<(), DbError> {
29 let remote = load_remote_object_on(conn, object_id)?;
30 let semantic_matches = match remote.semantic_payload() {
31 SemanticPayload::Carried(carried) => carried == expected_semantic_bytes,
32 SemanticPayload::Spooled(hash) => hash == ObjectHash::digest(expected_semantic_bytes),
33 SemanticPayload::Absent => false,
34 };
35 if remote.object() != expected_object || !semantic_matches {
36 return Err(DbError::Message(format!(
37 "prepared remote object {object_id} differs from its semantic index"
38 )));
39 }
40 Ok(())
41}
42
43pub(crate) fn load_remote_object_on(
44 conn: &Connection,
45 object_id: ObjectHash,
46) -> Result<RemoteObjectRecord, DbError> {
47 let state: String = conn
48 .query_row(
49 "SELECT state FROM remote_objects WHERE object_id = ?1",
50 [object_id.to_string()],
51 |row| row.get(0),
52 )
53 .map_err(|error| match error {
54 rusqlite::Error::QueryReturnedNoRows => {
55 DbError::Message(format!("prepared remote object {object_id} is absent"))
56 }
57 error => DbError::from(error),
58 })?;
59 let remote: RemoteObjectRecord = serde_json::from_str(&state).map_err(|error| {
60 DbError::context(
61 format!("prepared remote object {object_id} has invalid closed state"),
62 error,
63 )
64 })?;
65 remote
66 .validate()
67 .map_err(|error| DbError::context(format!("prepared remote object {object_id}"), error))?;
68 let actual = remote_object_id(remote.object());
69 if actual != object_id {
70 return Err(DbError::Message(format!(
71 "prepared remote object key is {object_id}, exact reference hashes to {actual}"
72 )));
73 }
74 let indexed = indexed_retained_replay_owners_on(conn, object_id)?;
75 let embedded = remote
76 .retained_replay_owners()
77 .cloned()
78 .collect::<BTreeSet<_>>();
79 if embedded != indexed {
80 return Err(DbError::Message(format!(
81 "prepared remote object {object_id} differs from its retained-replay ownership index"
82 )));
83 }
84 Ok(remote)
85}
86
87pub(crate) fn reopen_remote_object_on(
93 conn: &Connection,
94 store_dir: &StoreDir,
95 object_id: ObjectHash,
96) -> Result<coven_protocol::remote_object::ClosedRemoteObject, DbError> {
97 let remote = load_remote_object_on(conn, object_id)?;
98 let mut payloads = std::collections::BTreeMap::new();
99 for hash in remote.payload_claims() {
100 let bytes = crate::payload_store::read_payload_blocking(conn, store_dir, hash)
101 .map_err(DbError::from)?;
102 payloads.insert(hash, bytes);
103 }
104 coven_protocol::remote_object::ClosedRemoteObject::with_payloads(remote, payloads)
105 .map_err(|error| DbError::context(format!("remote object {object_id} payloads"), error))
106}
107
108pub(crate) fn indexed_retained_replay_owners_on(
109 conn: &Connection,
110 object_id: ObjectHash,
111) -> Result<BTreeSet<RetainedReplayOwner>, DbError> {
112 let mut statement = conn
113 .prepare(
114 "SELECT device_id, seq, commit_ref, input_hash
115 FROM retained_replay_objects WHERE object_id = ?1
116 ORDER BY device_id, seq",
117 )
118 .map_err(DbError::from)?;
119 let rows = statement
120 .query_map([object_id.to_string()], |row| {
121 Ok((
122 row.get::<_, String>(0)?,
123 row.get::<_, i64>(1)?,
124 row.get::<_, String>(2)?,
125 row.get::<_, String>(3)?,
126 ))
127 })
128 .map_err(DbError::from)?;
129 let mut owners = BTreeSet::new();
130 for row in rows {
131 let (device_id, sequence, encoded_commit, encoded_input_hash) =
132 row.map_err(DbError::from)?;
133 let commit: StoreBatchCommitRef =
134 serde_json::from_str(&encoded_commit).map_err(|error| {
135 DbError::context(
136 format!("retained replay object {object_id} commit ref"),
137 error,
138 )
139 })?;
140 let input_hash = encoded_input_hash.parse().map_err(|error| {
141 DbError::context(
142 format!("retained replay object {object_id} input hash"),
143 error,
144 )
145 })?;
146 let StoreCommitCoord {
147 stream_id,
148 sequence: commit_sequence,
149 } = &commit.coord;
150 let sequence = u64::try_from(sequence).map_err(|_| {
151 DbError::Message(format!(
152 "retained replay object {object_id} has an invalid sequence"
153 ))
154 })?;
155 if stream_id.to_string() != device_id || *commit_sequence != sequence {
156 return Err(DbError::Message(format!(
157 "retained replay object {object_id} index differs from its commit coordinate"
158 )));
159 }
160 if !owners.insert(RetainedReplayOwner::Commit { commit, input_hash }) {
161 return Err(DbError::Message(format!(
162 "retained replay object {object_id} repeats an owner"
163 )));
164 }
165 }
166 Ok(owners)
167}
168
169pub(crate) fn index_retained_replay_owner_on(
170 conn: &rusqlite::Transaction<'_>,
171 object_id: ObjectHash,
172 owner: &RetainedReplayOwner,
173) -> Result<(), DbError> {
174 let RetainedReplayOwner::Commit { commit, input_hash } = owner;
175 let StoreCommitCoord {
176 stream_id,
177 sequence,
178 } = &commit.coord;
179 let device_id = stream_id.to_string();
180 let sequence = Database::sequence_to_sqlite(&device_id, *sequence)?;
181 let commit_ref = serde_json::to_string(commit)
182 .map_err(|error| DbError::context("serialize retained replay commit ref", error))?;
183 let input_hash = input_hash.to_string();
184 conn.execute(
185 "INSERT INTO retained_replay_objects
186 (device_id, seq, commit_ref, input_hash, object_id)
187 VALUES (?1, ?2, ?3, ?4, ?5)
188 ON CONFLICT(device_id, seq, object_id) DO NOTHING",
189 rusqlite::params![
190 &device_id,
191 sequence,
192 &commit_ref,
193 &input_hash,
194 object_id.to_string()
195 ],
196 )
197 .map_err(DbError::from)?;
198 let stored: (String, String) = conn
199 .query_row(
200 "SELECT commit_ref, input_hash FROM retained_replay_objects
201 WHERE device_id = ?1 AND seq = ?2 AND object_id = ?3",
202 rusqlite::params![device_id, sequence, object_id.to_string()],
203 |row| Ok((row.get(0)?, row.get(1)?)),
204 )
205 .map_err(DbError::from)?;
206 if stored != (commit_ref, input_hash) {
207 return Err(DbError::Message(format!(
208 "retained replay object {object_id} already has different exact ownership"
209 )));
210 }
211 Ok(())
212}
213
214pub(crate) fn load_protocol_inert_object_on(
215 conn: &Connection,
216 object_id: ObjectHash,
217) -> Result<coven_protocol::remote_object::ProtocolInertObject, DbError> {
218 let state: String = conn
219 .query_row(
220 "SELECT state FROM protocol_inert_objects WHERE object_id = ?1",
221 [object_id.to_string()],
222 |row| row.get(0),
223 )
224 .map_err(DbError::from)?;
225 let inert: coven_protocol::remote_object::ProtocolInertObject = serde_json::from_str(&state)
226 .map_err(|error| {
227 DbError::context(
228 format!("protocol-inert object {object_id} has invalid closed state"),
229 error,
230 )
231 })?;
232 inert
233 .validate()
234 .map_err(|error| DbError::context(format!("protocol-inert object {object_id}"), error))?;
235 if inert.object_id() != object_id {
236 return Err(DbError::Message(format!(
237 "protocol-inert object key is {object_id}, exact reference hashes to {}",
238 inert.object_id()
239 )));
240 }
241 Ok(inert)
242}
243
244pub(crate) fn load_reclaimed_store_package_on(
245 conn: &Connection,
246 object_id: ObjectHash,
247) -> Result<Option<ReclaimedStorePackage>, DbError> {
248 let stored: Option<(String, String)> = conn
249 .query_row(
250 "SELECT authorization_hash, state FROM reclaimed_store_packages WHERE object_id = ?1",
251 [object_id.to_string()],
252 |row| Ok((row.get(0)?, row.get(1)?)),
253 )
254 .optional()
255 .map_err(DbError::from)?;
256 let Some((authorization_hash, state)) = stored else {
257 return Ok(None);
258 };
259 let authorization_hash = authorization_hash.parse::<ObjectHash>().map_err(|error| {
260 DbError::context(
261 format!("reclaimed Store package {object_id} has invalid authorization hash"),
262 error,
263 )
264 })?;
265 let reclaimed: ReclaimedStorePackage = serde_json::from_str(&state).map_err(|error| {
266 DbError::context(
267 format!("reclaimed Store package {object_id} has invalid closed state"),
268 error,
269 )
270 })?;
271 reclaimed.validate().map_err(store_reclaim_journal_error)?;
272 if reclaimed.object_id() != object_id
273 || reclaimed.authorization().authorization_hash != authorization_hash
274 {
275 return Err(DbError::Message(format!(
276 "reclaimed Store package {object_id} differs from its indexed identity"
277 )));
278 }
279 Ok(Some(reclaimed))
280}
281
282pub(crate) fn record_reclaimed_store_package_on(
283 conn: &Connection,
284 snapshot_root_hash: Option<ObjectHash>,
285 reclaimed: &ReclaimedStorePackage,
286) -> Result<(), DbError> {
287 reclaimed.validate().map_err(store_reclaim_journal_error)?;
288 let object_id = reclaimed.object_id();
289 if let Some(existing) = load_reclaimed_store_package_on(conn, object_id)? {
290 if existing == *reclaimed {
291 return Ok(());
292 }
293 if !matches!(
294 (&existing, reclaimed),
295 (
296 ReclaimedStorePackage::AbsentVerified {
297 authorization: existing_authorization,
298 authorization_activation: existing_activation,
299 },
300 ReclaimedStorePackage::Receipted {
301 authorization,
302 authorization_activation,
303 ..
304 }
305 ) if existing_authorization == authorization
306 && existing_activation == authorization_activation
307 ) {
308 return Err(DbError::Message(format!(
309 "reclaimed Store package {object_id} has conflicting closed authority"
310 )));
311 }
312 let state = serde_json::to_string(reclaimed)
313 .map_err(|error| DbError::context("serialize reclaimed Store package", error))?;
314 let updated = conn
315 .execute(
316 "UPDATE reclaimed_store_packages SET state = ?2 WHERE object_id = ?1",
317 (object_id.to_string(), state),
318 )
319 .map_err(DbError::from)?;
320 if updated != 1 {
321 return Err(DbError::Message(format!(
322 "reclaimed Store package {object_id} disappeared during receipt closure"
323 )));
324 }
325 return Ok(());
326 }
327
328 let remote_exists: bool = conn
329 .query_row(
330 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
331 [object_id.to_string()],
332 |row| row.get(0),
333 )
334 .map_err(DbError::from)?;
335 if remote_exists {
336 let remote = load_remote_object_on(conn, object_id)?;
337 match reclaimed.authorization().target() {
338 coven_protocol::reclaim::ReclaimTarget::StorePackage(target) => {
339 remote.validate_reclaimable_store_package(&target.package, &target.activation)
340 }
341 coven_protocol::reclaim::ReclaimTarget::CirclePackage(target) => {
342 remote.validate_reclaimable_circle_package(&target.package, &target.activation)
343 }
344 coven_protocol::reclaim::ReclaimTarget::CircleBootstrapImage(target) => remote
345 .validate_reclaimable_circle_bootstrap_image(
346 &target.coverage.bootstrap.image,
347 &target.coverage.activation_commit,
348 ),
349 coven_protocol::reclaim::ReclaimTarget::CircleSnapshotImage(target) => {
350 let root_hash = snapshot_root_hash.ok_or_else(|| {
351 DbError::Message(
352 "Circle snapshot reclaim closure has no verified Store root".to_string(),
353 )
354 })?;
355 let owner = target.snapshot_owner(root_hash).map_err(DbError::from)?;
356 remote.validate_reclaimable_snapshot_image(&target.image, &owner)
357 }
358 coven_protocol::reclaim::ReclaimTarget::StoreMembershipRollup(target) => remote
359 .validate_reclaimable_membership_rollup(&target.rollup, &target.snapshot_owner()),
360 coven_protocol::reclaim::ReclaimTarget::AudienceBlob(target) => {
361 remote.validate_reclaimable_stored_blob(&target.blob)
362 }
363 }
364 .map_err(|error| DbError::context(format!("close reclaimed package {object_id}"), error))?;
365 conn.execute(
371 "DELETE FROM row_blob_locators WHERE remote_object_id = ?1",
372 [object_id.to_string()],
373 )
374 .map_err(DbError::from)?;
375 conn.execute(
376 "DELETE FROM blob_locators WHERE remote_object_id = ?1",
377 [object_id.to_string()],
378 )
379 .map_err(DbError::from)?;
380 if !delete_remote_object_on(conn, object_id)? {
381 return Err(DbError::Message(format!(
382 "Store package {object_id} disappeared during reclaim closure"
383 )));
384 }
385 }
386 let inert_exists: bool = conn
387 .query_row(
388 "SELECT EXISTS(SELECT 1 FROM protocol_inert_objects WHERE object_id = ?1)",
389 [object_id.to_string()],
390 |row| row.get(0),
391 )
392 .map_err(DbError::from)?;
393 if inert_exists {
394 return Err(DbError::Message(format!(
395 "reclaimed Store package {object_id} is protocol-inert"
396 )));
397 }
398 let state = serde_json::to_string(reclaimed)
399 .map_err(|error| DbError::context("serialize reclaimed Store package", error))?;
400 let inserted = conn
401 .execute(
402 "INSERT INTO reclaimed_store_packages (object_id, authorization_hash, state) VALUES (?1, ?2, ?3)",
403 (
404 object_id.to_string(),
405 reclaimed.authorization().authorization_hash.to_string(),
406 state,
407 ),
408 )
409 .map_err(DbError::from)?;
410 if inserted != 1 {
411 return Err(DbError::Message(format!(
412 "reclaimed Store package {object_id} was not inserted"
413 )));
414 }
415 Ok(())
416}
417
418fn install_record_payloads_on(
425 conn: &Connection,
426 store_dir: &StoreDir,
427 closed: &ClosedRemoteObject,
428) -> Result<(), DbError> {
429 for (hash, bytes) in closed.payload_bytes() {
430 let written = crate::payload_store::write_payload_blocking(conn, store_dir, bytes)
431 .map_err(DbError::from)?;
432 if written != *hash {
433 return Err(DbError::Message(format!(
434 "remote object payload stored under {written}, named as {hash}"
435 )));
436 }
437 }
438 crate::payload_store::set_payload_owner_claims_on(
439 conn,
440 &crate::payload_store::remote_object_owner_key(closed.record().object_id()),
441 &closed.payload_bytes().keys().copied().collect(),
442 )
443}
444
445pub(crate) fn delete_remote_object_on(
452 conn: &Connection,
453 object_id: ObjectHash,
454) -> Result<bool, DbError> {
455 crate::payload_store::release_payload_owner_on(
456 conn,
457 &crate::payload_store::remote_object_owner_key(object_id),
458 )?;
459 let removed = conn
460 .execute(
461 "DELETE FROM remote_objects WHERE object_id = ?1",
462 [object_id.to_string()],
463 )
464 .map_err(DbError::from)?;
465 Ok(removed == 1)
466}
467
468pub(crate) fn persist_exact_remote_object_on(
469 conn: &Connection,
470 store_dir: &StoreDir,
471 closed: &ClosedRemoteObject,
472 domain: &str,
473) -> Result<(), DbError> {
474 let remote = closed.record();
475 remote
476 .validate()
477 .map_err(|error| DbError::context(format!("prepared {domain}"), error))?;
478 let object_id = remote.object_id();
479 ensure_remote_object_is_writable_on(conn, object_id, domain)?;
480 let existing = conn
481 .query_row(
482 "SELECT state FROM remote_objects WHERE object_id = ?1",
483 [object_id.to_string()],
484 |row| row.get::<_, String>(0),
485 )
486 .optional()
487 .map_err(DbError::from)?;
488 if let Some(existing) = existing {
489 let existing: RemoteObjectRecord = serde_json::from_str(&existing).map_err(|error| {
490 DbError::context(
491 format!("prepared {domain} {object_id} has invalid closed state"),
492 error,
493 )
494 })?;
495 if existing != *remote {
496 return Err(DbError::Message(format!(
497 "prepared {domain} {object_id} already has different closed state"
498 )));
499 }
500 return install_record_payloads_on(conn, store_dir, closed);
501 }
502 install_record_payloads_on(conn, store_dir, closed)?;
503 let state = serde_json::to_string(remote)
504 .map_err(|error| DbError::context(format!("serialize prepared {domain}"), error))?;
505 conn.execute(
506 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
507 (object_id.to_string(), state),
508 )
509 .map_err(DbError::from)?;
510 Ok(())
511}
512
513fn ensure_remote_object_is_writable_on(
514 conn: &Connection,
515 object_id: ObjectHash,
516 domain: &str,
517) -> Result<(), DbError> {
518 if load_reclaimed_store_package_on(conn, object_id)?.is_some() {
519 return Err(DbError::Message(format!(
520 "prepared {domain} {object_id} is a reclaimed Store package"
521 )));
522 }
523 let inert_exists: bool = conn
524 .query_row(
525 "SELECT EXISTS(
526 SELECT 1 FROM protocol_inert_objects WHERE object_id = ?1
527 )",
528 [object_id.to_string()],
529 |row| row.get(0),
530 )
531 .map_err(DbError::from)?;
532 if inert_exists {
533 return Err(DbError::Message(format!(
534 "prepared {domain} {object_id} is already protocol-inert"
535 )));
536 }
537 Ok(())
538}
539
540pub(crate) fn persist_prepared_remote_object_on(
541 conn: &Connection,
542 store_dir: &StoreDir,
543 closed: &ClosedRemoteObject,
544 owner: &StoreBatchCommitRef,
545 domain: &str,
546) -> Result<(), DbError> {
547 let remote = closed.record();
548 remote
549 .validate()
550 .map_err(|error| DbError::context(format!("prepared {domain}"), error))?;
551 let object_id = remote.object_id();
552 ensure_remote_object_is_writable_on(conn, object_id, domain)?;
553 let exists = conn
554 .query_row(
555 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
556 [object_id.to_string()],
557 |row| row.get::<_, bool>(0),
558 )
559 .map_err(DbError::from)?;
560 if !exists {
561 return persist_exact_remote_object_on(conn, store_dir, closed, domain);
562 }
563 let existing = load_remote_object_on(conn, object_id)?;
564 let merged = merge_prepared_remote_object(existing, remote, owner)?;
565 install_record_payloads_on(conn, store_dir, closed)?;
566 update_remote_object_on(conn, object_id, &merged)
567}
568
569pub(crate) fn update_remote_object_on(
570 conn: &Connection,
571 object_id: ObjectHash,
572 remote: &RemoteObjectRecord,
573) -> Result<(), DbError> {
574 remote
575 .validate()
576 .map_err(|error| DbError::context(format!("remote object {object_id}"), error))?;
577 if remote.object_id() != object_id {
578 return Err(DbError::Message(format!(
579 "remote object {object_id} changed its exact identity"
580 )));
581 }
582 let state = serde_json::to_string(remote)
583 .map_err(|error| DbError::context("serialize remote object", error))?;
584 let updated = conn
585 .execute(
586 "UPDATE remote_objects SET state = ?2 WHERE object_id = ?1",
587 (object_id.to_string(), state),
588 )
589 .map_err(DbError::from)?;
590 if updated != 1 {
591 return Err(DbError::Message(format!(
592 "remote object {object_id} disappeared during state transition"
593 )));
594 }
595 Ok(())
596}
597
598pub(crate) fn begin_remote_candidate_nonactivation_on(
599 conn: &rusqlite::Transaction<'_>,
600 object_id: ObjectHash,
601 nonactivation: coven_protocol::remote_object::CandidateNonactivation,
602) -> Result<Option<ExactObjectRef>, DbError> {
603 let mut remote = load_remote_object_on(conn, object_id)?;
604 let inert = remote
605 .begin_candidate_nonactivation(nonactivation)
606 .map_err(|error| {
607 DbError::context(
608 format!("record candidate nonactivation for {object_id}"),
609 error,
610 )
611 })?;
612 finish_remote_candidate_nonactivation_on(conn, object_id, remote, inert)
613}
614
615pub(crate) fn begin_remote_candidate_nonactivation_with_verified_head_on(
616 conn: &rusqlite::Transaction<'_>,
617 object_id: ObjectHash,
618 nonactivation: coven_protocol::remote_object::CandidateNonactivation,
619 head_nonactivation: &coven_protocol::remote_object::VerifiedCandidateHeadNonactivation,
620) -> Result<Option<ExactObjectRef>, DbError> {
621 let mut remote = load_remote_object_on(conn, object_id)?;
622 let inert = remote
623 .begin_candidate_nonactivation_with_verified_head_nonactivation(
624 nonactivation,
625 head_nonactivation,
626 )
627 .map_err(|error| {
628 DbError::context(
629 format!("record candidate nonactivation for {object_id}"),
630 error,
631 )
632 })?;
633 finish_remote_candidate_nonactivation_on(conn, object_id, remote, inert)
634}
635
636pub(crate) fn finish_remote_candidate_nonactivation_on(
637 conn: &rusqlite::Transaction<'_>,
638 object_id: ObjectHash,
639 remote: RemoteObjectRecord,
640 inert: Option<coven_protocol::remote_object::ProtocolInertObject>,
641) -> Result<Option<ExactObjectRef>, DbError> {
642 let Some(inert) = inert else {
643 let cleanup = remote.cleanup_target().cloned();
644 update_remote_object_on(conn, object_id, &remote)?;
645 return Ok(cleanup);
646 };
647 if inert.object_id() != object_id {
648 return Err(DbError::Message(format!(
649 "protocol-inert object {object_id} changed its exact identity"
650 )));
651 }
652 inert
653 .validate()
654 .map_err(|error| DbError::context(format!("protocol-inert object {object_id}"), error))?;
655 let encoded = serde_json::to_string(&inert)
656 .map_err(|error| DbError::context("serialize protocol-inert object", error))?;
657 if !delete_remote_object_on(conn, object_id)? {
658 return Err(DbError::Message(format!(
659 "remote object {object_id} disappeared during protocol-inert transition"
660 )));
661 }
662 let inserted = conn
663 .execute(
664 "INSERT INTO protocol_inert_objects (object_id, state) VALUES (?1, ?2)",
665 (object_id.to_string(), encoded),
666 )
667 .map_err(DbError::from)?;
668 if inserted != 1 {
669 return Err(DbError::Message(format!(
670 "protocol-inert object {object_id} was not inserted"
671 )));
672 }
673 Ok(None)
674}
675
676pub(crate) fn replace_prepared_merge_head_remote_on(
677 conn: &Connection,
678 store_dir: &StoreDir,
679 current: &ExactObjectRef,
680 winner: &StoreDeviceHead,
681 winner_object: &ExactObjectRef,
682 candidate: &StoreBatchCommitRef,
683) -> Result<(), DbError> {
684 let winner_bytes = winner.to_bytes();
687 if winner_object.verify(&winner_bytes).is_err()
688 || winner_object.slot() != current.slot()
689 || winner_object == current
690 || winner.commit != *candidate
691 {
692 return Err(DbError::Message(
693 "alternate Merge head does not replace the prepared activation slot".to_string(),
694 ));
695 }
696 let old_object_id = remote_object_id(current);
697 let old_remote = load_remote_object_on(conn, old_object_id)?;
698 if !matches!(
699 &old_remote,
700 RemoteObjectRecord::RetainedAuthority(record)
701 if matches!(
702 &record.identity.domain,
703 coven_protocol::remote_object::RetainedAuthorityObjectDomain::DeviceHead { .. }
704 ) && matches!(
705 &record.state,
706 coven_protocol::remote_object::RetainedAuthorityObjectState::Prepared { ownership }
707 if ownership.pending == BTreeSet::from([candidate.clone()])
708 )
709 ) {
710 return Err(DbError::Message(
711 "prepared Merge head lost its candidate ownership".to_string(),
712 ));
713 }
714 if !delete_remote_object_on(conn, old_object_id)? {
715 return Err(DbError::Message(
716 "prepared Merge head disappeared during replacement".to_string(),
717 ));
718 }
719 let winner_ref = coven_protocol::store_commit::StoreDeviceHeadRef {
720 head_hash: winner.head_hash(),
721 object: winner_object.clone(),
722 };
723 let winner_closed = RemoteObjectRecord::candidate_activated_store_head(
724 winner_ref,
725 &winner_bytes,
726 &winner_bytes,
727 candidate.clone(),
728 )
729 .map_err(|error| DbError::context("alternate Merge head", error))?;
730 let winner_closed = winner_closed
731 .map_record(|mut record| {
732 record.mark_uploaded_verified()?;
733 Ok(record)
734 })
735 .map_err(|error| DbError::context("mark alternate Merge head uploaded", error))?;
736 persist_exact_remote_object_on(conn, store_dir, &winner_closed, "alternate Merge head")
737}
738
739pub(crate) fn mark_remote_object_uploaded_on(
740 conn: &Connection,
741 expected: RemoteObjectRecord,
742) -> Result<RemoteObjectRecord, DbError> {
743 let object_id = expected.object_id();
744 let current = load_remote_object_on(conn, object_id)?;
745 if let (
746 RemoteObjectRecord::SharedLiveSet(current_record),
747 RemoteObjectRecord::CandidateExclusive(expected_record),
748 ) = (¤t, &expected)
749 {
750 let expected_owner = match &expected_record.state {
751 coven_protocol::remote_object::CandidateObjectState::Prepared { ownership }
752 | coven_protocol::remote_object::CandidateObjectState::UploadedVerified { ownership } => {
753 ownership.pending.iter().next()
754 }
755 coven_protocol::remote_object::CandidateObjectState::CleanupPending { .. }
756 | coven_protocol::remote_object::CandidateObjectState::AbsentVerified { .. } => None,
757 };
758 if expected_record.identity.domain.shared_destination()
759 == Some(current_record.identity.domain.clone())
760 && expected_record.identity.semantic_hash == current_record.identity.semantic_hash
761 && expected_record.identity.object == current_record.identity.object
762 && expected_record.payloads == current_record.payloads
763 && expected_owner.is_some_and(|owner| {
764 matches!(
765 ¤t_record.state,
766 coven_protocol::remote_object::OwnedObjectState::UploadedVerified { ownership }
767 if ownership.pending.contains(owner)
768 || ownership.activated.contains(
769 &coven_protocol::remote_object::SharedObjectOwner::StoreCommit(
770 owner.clone(),
771 ),
772 )
773 )
774 })
775 {
776 return Ok(current);
777 }
778 }
779 if let (
780 RemoteObjectRecord::RetainedAuthority(current_record),
781 RemoteObjectRecord::CandidateExclusive(expected_record),
782 ) = (¤t, &expected)
783 {
784 let expected_owner = match &expected_record.state {
785 coven_protocol::remote_object::CandidateObjectState::Prepared { ownership }
786 | coven_protocol::remote_object::CandidateObjectState::UploadedVerified { ownership } => {
787 ownership.pending.iter().next()
788 }
789 coven_protocol::remote_object::CandidateObjectState::CleanupPending { .. }
790 | coven_protocol::remote_object::CandidateObjectState::AbsentVerified { .. } => None,
791 };
792 if expected_record.identity.domain.retained_destination()
793 == Some(current_record.identity.domain.clone())
794 && expected_record.identity.semantic_hash == current_record.identity.semantic_hash
795 && expected_record.identity.object == current_record.identity.object
796 && expected_record.payloads == current_record.payloads
797 && expected_owner.is_some_and(|owner| {
798 matches!(
799 ¤t_record.state,
800 coven_protocol::remote_object::RetainedAuthorityObjectState::UploadedVerified {
801 ownership
802 } if ownership.pending.contains(owner) || ownership.activated.contains(owner)
803 )
804 })
805 {
806 return Ok(current);
807 }
808 }
809 let mut uploaded = expected.clone();
810 uploaded.mark_uploaded_verified().map_err(|error| {
811 DbError::context(format!("mark remote object {object_id} uploaded"), error)
812 })?;
813 if current == uploaded {
814 return Ok(current);
815 }
816 if current != expected {
817 return Err(DbError::Message(format!(
818 "remote object {object_id} changed before upload completion"
819 )));
820 }
821 let expected_json = serde_json::to_string(&expected)
822 .map_err(|error| DbError::context("serialize expected remote object", error))?;
823 let uploaded_json = serde_json::to_string(&uploaded)
824 .map_err(|error| DbError::context("serialize uploaded remote object", error))?;
825 let updated = conn
826 .execute(
827 "UPDATE remote_objects SET state = ?3
828 WHERE object_id = ?1 AND state = ?2",
829 (object_id.to_string(), expected_json, uploaded_json),
830 )
831 .map_err(DbError::from)?;
832 if updated != 1 {
833 return Err(DbError::Message(format!(
834 "remote object {object_id} lost upload ownership"
835 )));
836 }
837 Ok(uploaded)
838}
839
840pub(crate) fn mark_reusable_retained_authority_uploaded_on(
841 conn: &Connection,
842 expected: RemoteObjectRecord,
843) -> Result<RemoteObjectRecord, DbError> {
844 let object_id = expected.object_id();
845 let RemoteObjectRecord::RetainedAuthority(expected_record) = &expected else {
846 return Err(DbError::Message(format!(
847 "reusable remote object {object_id} is not retained authority"
848 )));
849 };
850 let coven_protocol::remote_object::RetainedAuthorityObjectState::Prepared {
851 ownership: expected_ownership,
852 } = &expected_record.state
853 else {
854 return Err(DbError::Message(format!(
855 "reusable retained authority {object_id} is not prepared"
856 )));
857 };
858 if expected_ownership.pending.len() != 1 || !expected_ownership.nonactivated.is_empty() {
859 return Err(DbError::Message(format!(
860 "reusable retained authority {object_id} has ambiguous expected ownership"
861 )));
862 }
863 let candidate = expected_ownership
864 .pending
865 .iter()
866 .next()
867 .expect("validated one expected candidate");
868 let mut current = load_remote_object_on(conn, object_id)?;
869 let RemoteObjectRecord::RetainedAuthority(current_record) = ¤t else {
870 return Err(DbError::Message(format!(
871 "reusable retained authority {object_id} changed domain"
872 )));
873 };
874 if current_record.identity != expected_record.identity
875 || current_record.payloads != expected_record.payloads
876 {
877 return Err(DbError::Message(format!(
878 "reusable retained authority {object_id} changed exact identity or bytes"
879 )));
880 }
881 let owns_candidate = match ¤t_record.state {
882 coven_protocol::remote_object::RetainedAuthorityObjectState::Prepared { ownership } => {
883 ownership.pending.contains(candidate)
884 }
885 coven_protocol::remote_object::RetainedAuthorityObjectState::UploadedVerified {
886 ownership,
887 } => ownership.pending.contains(candidate) || ownership.activated.contains(candidate),
888 coven_protocol::remote_object::RetainedAuthorityObjectState::CleanupPending { .. }
889 | coven_protocol::remote_object::RetainedAuthorityObjectState::AbsentVerified { .. }
890 | coven_protocol::remote_object::RetainedAuthorityObjectState::UncreatedVerified {
891 ..
892 } => false,
893 };
894 if !owns_candidate {
895 return Err(DbError::Message(format!(
896 "reusable retained authority {object_id} does not belong to its upload candidate"
897 )));
898 }
899 let before = current.clone();
900 current.mark_uploaded_verified().map_err(|error| {
901 DbError::context(
902 format!("mark reusable retained authority {object_id} uploaded"),
903 error,
904 )
905 })?;
906 if current != before {
907 update_remote_object_on(conn, object_id, ¤t)?;
908 }
909 Ok(current)
910}
911
912pub(crate) fn merge_prepared_remote_object(
913 existing: RemoteObjectRecord,
914 proposed: &RemoteObjectRecord,
915 owner: &StoreBatchCommitRef,
916) -> Result<RemoteObjectRecord, DbError> {
917 use coven_protocol::remote_object::{OwnedObjectState, SharedLiveSetObjectDomain};
918
919 if &existing == proposed {
920 return Ok(existing);
921 }
922 if let (
923 RemoteObjectRecord::SharedLiveSet(existing_record),
924 RemoteObjectRecord::CandidateExclusive(proposed_record),
925 ) = (&existing, proposed)
926 {
927 let proposed_owner = match &proposed_record.state {
928 coven_protocol::remote_object::CandidateObjectState::Prepared { ownership }
929 | coven_protocol::remote_object::CandidateObjectState::UploadedVerified { ownership } => {
930 ownership.pending.contains(owner)
931 }
932 coven_protocol::remote_object::CandidateObjectState::CleanupPending { .. }
933 | coven_protocol::remote_object::CandidateObjectState::AbsentVerified { .. } => false,
934 };
935 if proposed_record.identity.domain.shared_destination()
936 != Some(existing_record.identity.domain.clone())
937 || proposed_record.identity.semantic_hash != existing_record.identity.semantic_hash
938 || proposed_record.identity.object != existing_record.identity.object
939 || proposed_record.payloads != existing_record.payloads
940 || !proposed_owner
941 {
942 return Err(DbError::Message(format!(
943 "shared candidate object {} already has different identity, bytes, or ownership",
944 proposed.object_id()
945 )));
946 }
947 let mut merged = existing.clone();
948 let RemoteObjectRecord::SharedLiveSet(record) = &mut merged else {
949 unreachable!("matched shared live-set object")
950 };
951 match &mut record.state {
952 OwnedObjectState::Prepared { ownership } => {
953 ownership.pending.insert(owner.clone());
954 }
955 OwnedObjectState::UploadedVerified { ownership } => {
956 ownership.pending.insert(owner.clone());
957 }
958 OwnedObjectState::RetirementPending { former_candidates } => {
959 record.state = OwnedObjectState::UploadedVerified {
960 ownership: coven_protocol::remote_object::SharedObjectOwnership {
961 pending: BTreeSet::from([owner.clone()]),
962 activated: BTreeSet::new(),
963 nonactivated: former_candidates.clone(),
964 },
965 };
966 }
967 }
968 merged.validate().map_err(|error| {
969 DbError::context(
970 format!("merge shared candidate object {}", proposed.object_id()),
971 error,
972 )
973 })?;
974 return Ok(merged);
975 }
976 if let (
977 RemoteObjectRecord::RetainedAuthority(existing_record),
978 RemoteObjectRecord::CandidateExclusive(proposed_record),
979 ) = (&existing, proposed)
980 {
981 if proposed_record.identity.domain.retained_destination()
982 != Some(existing_record.identity.domain.clone())
983 || proposed_record.identity.semantic_hash != existing_record.identity.semantic_hash
984 || proposed_record.identity.object != existing_record.identity.object
985 || proposed_record.payloads != existing_record.payloads
986 {
987 return Err(DbError::Message(format!(
988 "retained candidate object {} already has different identity or bytes",
989 proposed.object_id()
990 )));
991 }
992 let proposed_owner = match &proposed_record.state {
993 coven_protocol::remote_object::CandidateObjectState::Prepared { ownership }
994 | coven_protocol::remote_object::CandidateObjectState::UploadedVerified { ownership } => {
995 ownership.pending.contains(owner)
996 }
997 coven_protocol::remote_object::CandidateObjectState::CleanupPending { .. }
998 | coven_protocol::remote_object::CandidateObjectState::AbsentVerified { .. } => false,
999 };
1000 if !proposed_owner {
1001 return Err(DbError::Message(format!(
1002 "retained candidate object {} does not name its preparing commit",
1003 proposed.object_id()
1004 )));
1005 }
1006 let mut merged = existing.clone();
1007 merged
1008 .add_retained_authority_candidate(owner.clone())
1009 .map_err(|error| {
1010 DbError::context(
1011 format!("merge retained candidate object {}", proposed.object_id()),
1012 error,
1013 )
1014 })?;
1015 return Ok(merged);
1016 }
1017 let (
1018 RemoteObjectRecord::SharedLiveSet(mut existing),
1019 RemoteObjectRecord::SharedLiveSet(proposed),
1020 ) = (existing, proposed)
1021 else {
1022 return Err(DbError::Message(format!(
1023 "remote object {} already has different closed state",
1024 proposed.object_id()
1025 )));
1026 };
1027 if existing.identity.domain != SharedLiveSetObjectDomain::StoredBlob
1028 || proposed.identity.domain != SharedLiveSetObjectDomain::StoredBlob
1029 || existing.identity != proposed.identity
1030 || existing.payloads != proposed.payloads
1031 {
1032 return Err(DbError::Message(format!(
1033 "stored blob object {} already has different identity or bytes",
1034 remote_object_id(&proposed.identity.object)
1035 )));
1036 }
1037 let proposed_has_owner = match &proposed.state {
1038 OwnedObjectState::Prepared { ownership } => ownership.pending.contains(owner),
1039 OwnedObjectState::UploadedVerified { ownership } => ownership.pending.contains(owner),
1040 OwnedObjectState::RetirementPending { .. } => false,
1041 };
1042 if !proposed_has_owner {
1043 return Err(DbError::Message(format!(
1044 "stored blob object {} does not name its preparing commit",
1045 remote_object_id(&proposed.identity.object)
1046 )));
1047 }
1048 let proposed_uploaded = matches!(&proposed.state, OwnedObjectState::UploadedVerified { .. });
1049 match &mut existing.state {
1050 OwnedObjectState::Prepared { ownership } => {
1051 ownership.pending.insert(owner.clone());
1052 if proposed_uploaded {
1053 existing.state = OwnedObjectState::UploadedVerified {
1054 ownership: coven_protocol::remote_object::SharedObjectOwnership {
1055 pending: ownership.pending.clone(),
1056 activated: std::collections::BTreeSet::new(),
1057 nonactivated: ownership.nonactivated.clone(),
1058 },
1059 };
1060 }
1061 }
1062 OwnedObjectState::UploadedVerified { ownership } => {
1063 ownership.pending.insert(owner.clone());
1064 }
1065 OwnedObjectState::RetirementPending { former_candidates } => {
1066 let ownership = coven_protocol::remote_object::PendingCandidateOwnership {
1067 pending: std::collections::BTreeSet::from([owner.clone()]),
1068 nonactivated: former_candidates.clone(),
1069 };
1070 existing.state = if proposed_uploaded {
1071 OwnedObjectState::UploadedVerified {
1072 ownership: coven_protocol::remote_object::SharedObjectOwnership {
1073 pending: ownership.pending,
1074 activated: std::collections::BTreeSet::new(),
1075 nonactivated: ownership.nonactivated,
1076 },
1077 }
1078 } else {
1079 OwnedObjectState::Prepared { ownership }
1080 };
1081 }
1082 }
1083 let merged = RemoteObjectRecord::SharedLiveSet(existing);
1084 merged.validate().map_err(|error| {
1085 DbError::context(
1086 format!("merged stored blob object {}", merged.object_id()),
1087 error,
1088 )
1089 })?;
1090 Ok(merged)
1091}
1092
1093pub(crate) fn validate_prepared_package_on(
1094 conn: &Connection,
1095 store_dir: &StoreDir,
1096 write_id: &WriteId,
1097 expected: &PreparedAudiencePackage,
1098) -> Result<(), DbError> {
1099 let audience = expected.package().audience().remote_audience();
1100 let remote_object_id: String = conn
1101 .query_row(
1102 "SELECT remote_object_id
1103 FROM store_write_packages
1104 WHERE write_id = ?1 AND audience = ?2",
1105 rusqlite::params![write_id.as_str(), remote_audience_to_db(&audience)],
1106 |row| row.get(0),
1107 )
1108 .map_err(DbError::from)?;
1109 let remote_object_id = remote_object_id
1110 .parse()
1111 .map_err(|error| DbError::context("stored prepared remote object id is invalid", error))?;
1112 let actual = PreparedAudiencePackage::from_remote(
1113 conn,
1114 store_dir,
1115 load_remote_object_on(conn, remote_object_id)?,
1116 )?;
1117 if actual.package() != expected.package()
1118 || actual.semantic_bytes() != expected.semantic_bytes()
1119 || actual.stored_bytes() != expected.stored_bytes()
1120 || actual.object() != expected.object()
1121 || actual.remote_object_id() != expected.remote_object_id()
1122 {
1123 return Err(DbError::Message(format!(
1124 "write {write_id} audience {audience:?} already has different prepared package bytes"
1125 )));
1126 }
1127 Ok(())
1128}
1129
1130pub(crate) fn validate_prepared_blob_on(
1131 conn: &Connection,
1132 write_id: &WriteId,
1133 expected: &PreparedAudienceBlob,
1134) -> Result<(), DbError> {
1135 let locator_hash = expected.blob().locator().locator_hash();
1136 let remote_object_id = expected.remote_object_id();
1137 let (stored_locator_hash, spool_path): (String, Option<String>) = conn
1138 .query_row(
1139 "SELECT locator_hash, spool_path
1140 FROM store_write_blobs
1141 WHERE write_id = ?1 AND audience = ?2 AND remote_object_id = ?3",
1142 rusqlite::params![
1143 write_id.as_str(),
1144 remote_audience_to_db(expected.audience()),
1145 remote_object_id.to_string(),
1146 ],
1147 |row| Ok((row.get(0)?, row.get(1)?)),
1148 )
1149 .map_err(DbError::from)?;
1150 if stored_locator_hash != locator_hash.to_string() {
1151 return Err(DbError::Message(format!(
1152 "write {write_id} audience {:?} exact object {remote_object_id} is indexed under locator {stored_locator_hash}, expected {locator_hash}",
1153 expected.audience()
1154 )));
1155 }
1156 let actual = PreparedAudienceBlob::from_remote(
1157 expected.audience().clone(),
1158 &locator_hash.to_string(),
1159 load_remote_object_on(conn, remote_object_id)?,
1160 spool_path.map(PathBuf::from),
1161 )?;
1162 if actual.blob() != expected.blob()
1163 || actual.spool_path() != expected.spool_path()
1164 || actual.remote_object_id() != expected.remote_object_id()
1165 {
1166 return Err(DbError::Message(format!(
1167 "write {write_id} audience {:?} exact object {remote_object_id} already has different prepared blob bytes",
1168 expected.audience()
1169 )));
1170 }
1171 Ok(())
1172}