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
214#[cfg(any(test, feature = "test-utils"))]
215pub(crate) fn load_protocol_inert_object_on(
216 conn: &Connection,
217 object_id: ObjectHash,
218) -> Result<coven_protocol::remote_object::ProtocolInertObject, DbError> {
219 let state: String = conn
220 .query_row(
221 "SELECT state FROM protocol_inert_objects WHERE object_id = ?1",
222 [object_id.to_string()],
223 |row| row.get(0),
224 )
225 .map_err(DbError::from)?;
226 let inert: coven_protocol::remote_object::ProtocolInertObject = serde_json::from_str(&state)
227 .map_err(|error| {
228 DbError::context(
229 format!("protocol-inert object {object_id} has invalid closed state"),
230 error,
231 )
232 })?;
233 inert
234 .validate()
235 .map_err(|error| DbError::context(format!("protocol-inert object {object_id}"), error))?;
236 if inert.object_id() != object_id {
237 return Err(DbError::Message(format!(
238 "protocol-inert object key is {object_id}, exact reference hashes to {}",
239 inert.object_id()
240 )));
241 }
242 Ok(inert)
243}
244
245pub(crate) fn load_reclaimed_store_package_on(
246 conn: &Connection,
247 object_id: ObjectHash,
248) -> Result<Option<ReclaimedStorePackage>, DbError> {
249 let stored: Option<(String, String)> = conn
250 .query_row(
251 "SELECT authorization_hash, state FROM reclaimed_store_packages WHERE object_id = ?1",
252 [object_id.to_string()],
253 |row| Ok((row.get(0)?, row.get(1)?)),
254 )
255 .optional()
256 .map_err(DbError::from)?;
257 let Some((authorization_hash, state)) = stored else {
258 return Ok(None);
259 };
260 let authorization_hash = authorization_hash.parse::<ObjectHash>().map_err(|error| {
261 DbError::context(
262 format!("reclaimed Store package {object_id} has invalid authorization hash"),
263 error,
264 )
265 })?;
266 let reclaimed: ReclaimedStorePackage = serde_json::from_str(&state).map_err(|error| {
267 DbError::context(
268 format!("reclaimed Store package {object_id} has invalid closed state"),
269 error,
270 )
271 })?;
272 reclaimed.validate().map_err(store_reclaim_journal_error)?;
273 if reclaimed.object_id() != object_id
274 || reclaimed.authorization().authorization_hash != authorization_hash
275 {
276 return Err(DbError::Message(format!(
277 "reclaimed Store package {object_id} differs from its indexed identity"
278 )));
279 }
280 Ok(Some(reclaimed))
281}
282
283pub(crate) fn record_reclaimed_store_package_on(
284 conn: &Connection,
285 snapshot_root_hash: Option<ObjectHash>,
286 reclaimed: &ReclaimedStorePackage,
287) -> Result<(), DbError> {
288 reclaimed.validate().map_err(store_reclaim_journal_error)?;
289 let object_id = reclaimed.object_id();
290 if let Some(existing) = load_reclaimed_store_package_on(conn, object_id)? {
291 if existing == *reclaimed {
292 return Ok(());
293 }
294 if !matches!(
295 (&existing, reclaimed),
296 (
297 ReclaimedStorePackage::AbsentVerified {
298 authorization: existing_authorization,
299 authorization_activation: existing_activation,
300 },
301 ReclaimedStorePackage::Receipted {
302 authorization,
303 authorization_activation,
304 ..
305 }
306 ) if existing_authorization == authorization
307 && existing_activation == authorization_activation
308 ) {
309 return Err(DbError::Message(format!(
310 "reclaimed Store package {object_id} has conflicting closed authority"
311 )));
312 }
313 let state = serde_json::to_string(reclaimed)
314 .map_err(|error| DbError::context("serialize reclaimed Store package", error))?;
315 let updated = conn
316 .execute(
317 "UPDATE reclaimed_store_packages SET state = ?2 WHERE object_id = ?1",
318 (object_id.to_string(), state),
319 )
320 .map_err(DbError::from)?;
321 if updated != 1 {
322 return Err(DbError::Message(format!(
323 "reclaimed Store package {object_id} disappeared during receipt closure"
324 )));
325 }
326 return Ok(());
327 }
328
329 let remote_exists: bool = conn
330 .query_row(
331 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
332 [object_id.to_string()],
333 |row| row.get(0),
334 )
335 .map_err(DbError::from)?;
336 if remote_exists {
337 let remote = load_remote_object_on(conn, object_id)?;
338 match reclaimed.authorization().target() {
339 coven_protocol::reclaim::ReclaimTarget::StorePackage(target) => {
340 remote.validate_reclaimable_store_package(&target.package, &target.activation)
341 }
342 coven_protocol::reclaim::ReclaimTarget::CirclePackage(target) => {
343 remote.validate_reclaimable_circle_package(&target.package, &target.activation)
344 }
345 coven_protocol::reclaim::ReclaimTarget::CircleBootstrapImage(target) => remote
346 .validate_reclaimable_circle_bootstrap_image(
347 &target.coverage.bootstrap.image,
348 &target.coverage.activation_commit,
349 ),
350 coven_protocol::reclaim::ReclaimTarget::CircleSnapshotImage(target) => {
351 let root_hash = snapshot_root_hash.ok_or_else(|| {
352 DbError::Message(
353 "Circle snapshot reclaim closure has no verified Store root".to_string(),
354 )
355 })?;
356 let owner = target.snapshot_owner(root_hash).map_err(DbError::from)?;
357 remote.validate_reclaimable_snapshot_image(&target.image, &owner)
358 }
359 coven_protocol::reclaim::ReclaimTarget::AudienceBlob(target) => {
360 remote.validate_reclaimable_stored_blob(target.blob())
361 }
362 }
363 .map_err(|error| DbError::context(format!("close reclaimed package {object_id}"), error))?;
364 conn.execute(
370 "DELETE FROM row_blob_locators WHERE remote_object_id = ?1",
371 [object_id.to_string()],
372 )
373 .map_err(DbError::from)?;
374 conn.execute(
375 "DELETE FROM blob_locators WHERE remote_object_id = ?1",
376 [object_id.to_string()],
377 )
378 .map_err(DbError::from)?;
379 if !delete_remote_object_on(conn, object_id)? {
380 return Err(DbError::Message(format!(
381 "Store package {object_id} disappeared during reclaim closure"
382 )));
383 }
384 }
385 let inert_exists: bool = conn
386 .query_row(
387 "SELECT EXISTS(SELECT 1 FROM protocol_inert_objects WHERE object_id = ?1)",
388 [object_id.to_string()],
389 |row| row.get(0),
390 )
391 .map_err(DbError::from)?;
392 if inert_exists {
393 return Err(DbError::Message(format!(
394 "reclaimed Store package {object_id} is protocol-inert"
395 )));
396 }
397 let state = serde_json::to_string(reclaimed)
398 .map_err(|error| DbError::context("serialize reclaimed Store package", error))?;
399 let inserted = conn
400 .execute(
401 "INSERT INTO reclaimed_store_packages (object_id, authorization_hash, state) VALUES (?1, ?2, ?3)",
402 (
403 object_id.to_string(),
404 reclaimed.authorization().authorization_hash.to_string(),
405 state,
406 ),
407 )
408 .map_err(DbError::from)?;
409 if inserted != 1 {
410 return Err(DbError::Message(format!(
411 "reclaimed Store package {object_id} was not inserted"
412 )));
413 }
414 Ok(())
415}
416
417fn install_record_payloads_on(
424 conn: &Connection,
425 store_dir: &StoreDir,
426 closed: &ClosedRemoteObject,
427) -> Result<(), DbError> {
428 for (hash, bytes) in closed.payload_bytes() {
429 let written = crate::payload_store::write_payload_blocking(conn, store_dir, bytes)
430 .map_err(DbError::from)?;
431 if written != *hash {
432 return Err(DbError::Message(format!(
433 "remote object payload stored under {written}, named as {hash}"
434 )));
435 }
436 }
437 crate::payload_store::set_payload_owner_claims_on(
438 conn,
439 &crate::payload_store::remote_object_owner_key(closed.record().object_id()),
440 &closed.payload_bytes().keys().copied().collect(),
441 )
442}
443
444pub(crate) fn delete_remote_object_on(
451 conn: &Connection,
452 object_id: ObjectHash,
453) -> Result<bool, DbError> {
454 crate::payload_store::release_payload_owner_on(
455 conn,
456 &crate::payload_store::remote_object_owner_key(object_id),
457 )?;
458 let removed = conn
459 .execute(
460 "DELETE FROM remote_objects WHERE object_id = ?1",
461 [object_id.to_string()],
462 )
463 .map_err(DbError::from)?;
464 Ok(removed == 1)
465}
466
467pub(crate) fn persist_exact_remote_object_on(
468 conn: &Connection,
469 store_dir: &StoreDir,
470 closed: &ClosedRemoteObject,
471 domain: &str,
472) -> Result<(), DbError> {
473 let remote = closed.record();
474 remote
475 .validate()
476 .map_err(|error| DbError::context(format!("prepared {domain}"), error))?;
477 let object_id = remote.object_id();
478 ensure_remote_object_is_writable_on(conn, object_id, domain)?;
479 let existing = conn
480 .query_row(
481 "SELECT state FROM remote_objects WHERE object_id = ?1",
482 [object_id.to_string()],
483 |row| row.get::<_, String>(0),
484 )
485 .optional()
486 .map_err(DbError::from)?;
487 if let Some(existing) = existing {
488 let existing: RemoteObjectRecord = serde_json::from_str(&existing).map_err(|error| {
489 DbError::context(
490 format!("prepared {domain} {object_id} has invalid closed state"),
491 error,
492 )
493 })?;
494 if existing != *remote {
495 return Err(DbError::Message(format!(
496 "prepared {domain} {object_id} already has different closed state"
497 )));
498 }
499 return install_record_payloads_on(conn, store_dir, closed);
500 }
501 install_record_payloads_on(conn, store_dir, closed)?;
502 let state = serde_json::to_string(remote)
503 .map_err(|error| DbError::context(format!("serialize prepared {domain}"), error))?;
504 conn.execute(
505 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
506 (object_id.to_string(), state),
507 )
508 .map_err(DbError::from)?;
509 Ok(())
510}
511
512fn ensure_remote_object_is_writable_on(
513 conn: &Connection,
514 object_id: ObjectHash,
515 domain: &str,
516) -> Result<(), DbError> {
517 if load_reclaimed_store_package_on(conn, object_id)?.is_some() {
518 return Err(DbError::Message(format!(
519 "prepared {domain} {object_id} is a reclaimed Store package"
520 )));
521 }
522 let inert_exists: bool = conn
523 .query_row(
524 "SELECT EXISTS(
525 SELECT 1 FROM protocol_inert_objects WHERE object_id = ?1
526 )",
527 [object_id.to_string()],
528 |row| row.get(0),
529 )
530 .map_err(DbError::from)?;
531 if inert_exists {
532 return Err(DbError::Message(format!(
533 "prepared {domain} {object_id} is already protocol-inert"
534 )));
535 }
536 Ok(())
537}
538
539pub(crate) fn persist_prepared_remote_object_on(
540 conn: &Connection,
541 store_dir: &StoreDir,
542 closed: &ClosedRemoteObject,
543 owner: &StoreBatchCommitRef,
544 domain: &str,
545) -> Result<(), DbError> {
546 let remote = closed.record();
547 remote
548 .validate()
549 .map_err(|error| DbError::context(format!("prepared {domain}"), error))?;
550 let object_id = remote.object_id();
551 ensure_remote_object_is_writable_on(conn, object_id, domain)?;
552 let exists = conn
553 .query_row(
554 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
555 [object_id.to_string()],
556 |row| row.get::<_, bool>(0),
557 )
558 .map_err(DbError::from)?;
559 if !exists {
560 return persist_exact_remote_object_on(conn, store_dir, closed, domain);
561 }
562 let existing = load_remote_object_on(conn, object_id)?;
563 let merged = merge_prepared_remote_object(existing, remote, owner)?;
564 install_record_payloads_on(conn, store_dir, closed)?;
565 update_remote_object_on(conn, object_id, &merged)
566}
567
568pub(crate) fn update_remote_object_on(
569 conn: &Connection,
570 object_id: ObjectHash,
571 remote: &RemoteObjectRecord,
572) -> Result<(), DbError> {
573 remote
574 .validate()
575 .map_err(|error| DbError::context(format!("remote object {object_id}"), error))?;
576 if remote.object_id() != object_id {
577 return Err(DbError::Message(format!(
578 "remote object {object_id} changed its exact identity"
579 )));
580 }
581 let state = serde_json::to_string(remote)
582 .map_err(|error| DbError::context("serialize remote object", error))?;
583 let updated = conn
584 .execute(
585 "UPDATE remote_objects SET state = ?2 WHERE object_id = ?1",
586 (object_id.to_string(), state),
587 )
588 .map_err(DbError::from)?;
589 if updated != 1 {
590 return Err(DbError::Message(format!(
591 "remote object {object_id} disappeared during state transition"
592 )));
593 }
594 Ok(())
595}
596
597pub(crate) fn begin_remote_candidate_nonactivation_on(
598 conn: &rusqlite::Transaction<'_>,
599 object_id: ObjectHash,
600 nonactivation: coven_protocol::remote_object::CandidateNonactivation,
601) -> Result<Option<ExactObjectRef>, DbError> {
602 let mut remote = load_remote_object_on(conn, object_id)?;
603 let inert = remote
604 .begin_candidate_nonactivation(nonactivation)
605 .map_err(|error| {
606 DbError::context(
607 format!("record candidate nonactivation for {object_id}"),
608 error,
609 )
610 })?;
611 finish_remote_candidate_nonactivation_on(conn, object_id, remote, inert)
612}
613
614pub(crate) fn finish_remote_candidate_nonactivation_on(
615 conn: &rusqlite::Transaction<'_>,
616 object_id: ObjectHash,
617 remote: RemoteObjectRecord,
618 inert: Option<coven_protocol::remote_object::ProtocolInertObject>,
619) -> Result<Option<ExactObjectRef>, DbError> {
620 let Some(inert) = inert else {
621 let cleanup = remote.cleanup_target().cloned();
622 update_remote_object_on(conn, object_id, &remote)?;
623 return Ok(cleanup);
624 };
625 if inert.object_id() != object_id {
626 return Err(DbError::Message(format!(
627 "protocol-inert object {object_id} changed its exact identity"
628 )));
629 }
630 inert
631 .validate()
632 .map_err(|error| DbError::context(format!("protocol-inert object {object_id}"), error))?;
633 let encoded = serde_json::to_string(&inert)
634 .map_err(|error| DbError::context("serialize protocol-inert object", error))?;
635 if !delete_remote_object_on(conn, object_id)? {
636 return Err(DbError::Message(format!(
637 "remote object {object_id} disappeared during protocol-inert transition"
638 )));
639 }
640 let inserted = conn
641 .execute(
642 "INSERT INTO protocol_inert_objects (object_id, state) VALUES (?1, ?2)",
643 (object_id.to_string(), encoded),
644 )
645 .map_err(DbError::from)?;
646 if inserted != 1 {
647 return Err(DbError::Message(format!(
648 "protocol-inert object {object_id} was not inserted"
649 )));
650 }
651 Ok(None)
652}
653
654pub(crate) fn mark_remote_object_uploaded_on(
655 conn: &Connection,
656 expected: RemoteObjectRecord,
657) -> Result<RemoteObjectRecord, DbError> {
658 let object_id = expected.object_id();
659 let current = load_remote_object_on(conn, object_id)?;
660 if let (
661 RemoteObjectRecord::SharedLiveSet(current_record),
662 RemoteObjectRecord::CandidateExclusive(expected_record),
663 ) = (¤t, &expected)
664 {
665 let expected_owner = match &expected_record.state {
666 coven_protocol::remote_object::CandidateObjectState::Prepared { ownership }
667 | coven_protocol::remote_object::CandidateObjectState::UploadedVerified { ownership } => {
668 ownership.pending.iter().next()
669 }
670 coven_protocol::remote_object::CandidateObjectState::CleanupPending { .. }
671 | coven_protocol::remote_object::CandidateObjectState::AbsentVerified { .. } => None,
672 };
673 if expected_record.identity.domain.shared_destination()
674 == Some(current_record.identity.domain.clone())
675 && expected_record.identity.semantic_hash == current_record.identity.semantic_hash
676 && expected_record.identity.object == current_record.identity.object
677 && expected_record.payloads == current_record.payloads
678 && expected_owner.is_some_and(|owner| {
679 matches!(
680 ¤t_record.state,
681 coven_protocol::remote_object::OwnedObjectState::UploadedVerified { ownership }
682 if ownership.pending.contains(owner)
683 || ownership.activated.contains(
684 &coven_protocol::remote_object::SharedObjectOwner::StoreCommit(
685 owner.clone(),
686 ),
687 )
688 )
689 })
690 {
691 return Ok(current);
692 }
693 }
694 if let (
695 RemoteObjectRecord::RetainedAuthority(current_record),
696 RemoteObjectRecord::CandidateExclusive(expected_record),
697 ) = (¤t, &expected)
698 {
699 let expected_owner = match &expected_record.state {
700 coven_protocol::remote_object::CandidateObjectState::Prepared { ownership }
701 | coven_protocol::remote_object::CandidateObjectState::UploadedVerified { ownership } => {
702 ownership.pending.iter().next()
703 }
704 coven_protocol::remote_object::CandidateObjectState::CleanupPending { .. }
705 | coven_protocol::remote_object::CandidateObjectState::AbsentVerified { .. } => None,
706 };
707 if expected_record.identity.domain.retained_destination()
708 == Some(current_record.identity.domain.clone())
709 && expected_record.identity.semantic_hash == current_record.identity.semantic_hash
710 && expected_record.identity.object == current_record.identity.object
711 && expected_record.payloads == current_record.payloads
712 && expected_owner.is_some_and(|owner| {
713 matches!(
714 ¤t_record.state,
715 coven_protocol::remote_object::RetainedAuthorityObjectState::UploadedVerified {
716 ownership
717 } if ownership.pending.contains(owner) || ownership.activated.contains(owner)
718 )
719 })
720 {
721 return Ok(current);
722 }
723 }
724 let mut uploaded = expected.clone();
725 uploaded.mark_uploaded_verified().map_err(|error| {
726 DbError::context(format!("mark remote object {object_id} uploaded"), error)
727 })?;
728 if current == uploaded {
729 return Ok(current);
730 }
731 if current != expected {
732 return Err(DbError::Message(format!(
733 "remote object {object_id} changed before upload completion"
734 )));
735 }
736 let expected_json = serde_json::to_string(&expected)
737 .map_err(|error| DbError::context("serialize expected remote object", error))?;
738 let uploaded_json = serde_json::to_string(&uploaded)
739 .map_err(|error| DbError::context("serialize uploaded remote object", error))?;
740 let updated = conn
741 .execute(
742 "UPDATE remote_objects SET state = ?3
743 WHERE object_id = ?1 AND state = ?2",
744 (object_id.to_string(), expected_json, uploaded_json),
745 )
746 .map_err(DbError::from)?;
747 if updated != 1 {
748 return Err(DbError::Message(format!(
749 "remote object {object_id} lost upload ownership"
750 )));
751 }
752 Ok(uploaded)
753}
754
755pub(crate) fn mark_reusable_retained_authority_uploaded_on(
756 conn: &Connection,
757 expected: RemoteObjectRecord,
758) -> Result<RemoteObjectRecord, DbError> {
759 let object_id = expected.object_id();
760 let RemoteObjectRecord::RetainedAuthority(expected_record) = &expected else {
761 return Err(DbError::Message(format!(
762 "reusable remote object {object_id} is not retained authority"
763 )));
764 };
765 let coven_protocol::remote_object::RetainedAuthorityObjectState::Prepared {
766 ownership: expected_ownership,
767 } = &expected_record.state
768 else {
769 return Err(DbError::Message(format!(
770 "reusable retained authority {object_id} is not prepared"
771 )));
772 };
773 if expected_ownership.pending.len() != 1 || !expected_ownership.nonactivated.is_empty() {
774 return Err(DbError::Message(format!(
775 "reusable retained authority {object_id} has ambiguous expected ownership"
776 )));
777 }
778 let candidate = expected_ownership
779 .pending
780 .iter()
781 .next()
782 .expect("validated one expected candidate");
783 let mut current = load_remote_object_on(conn, object_id)?;
784 let RemoteObjectRecord::RetainedAuthority(current_record) = ¤t else {
785 return Err(DbError::Message(format!(
786 "reusable retained authority {object_id} changed domain"
787 )));
788 };
789 if current_record.identity != expected_record.identity
790 || current_record.payloads != expected_record.payloads
791 {
792 return Err(DbError::Message(format!(
793 "reusable retained authority {object_id} changed exact identity or bytes"
794 )));
795 }
796 let owns_candidate = match ¤t_record.state {
797 coven_protocol::remote_object::RetainedAuthorityObjectState::Prepared { ownership } => {
798 ownership.pending.contains(candidate)
799 }
800 coven_protocol::remote_object::RetainedAuthorityObjectState::UploadedVerified {
801 ownership,
802 } => ownership.pending.contains(candidate) || ownership.activated.contains(candidate),
803 };
804 if !owns_candidate {
805 return Err(DbError::Message(format!(
806 "reusable retained authority {object_id} does not belong to its upload candidate"
807 )));
808 }
809 let before = current.clone();
810 current.mark_uploaded_verified().map_err(|error| {
811 DbError::context(
812 format!("mark reusable retained authority {object_id} uploaded"),
813 error,
814 )
815 })?;
816 if current != before {
817 update_remote_object_on(conn, object_id, ¤t)?;
818 }
819 Ok(current)
820}
821
822pub(crate) fn merge_prepared_remote_object(
823 existing: RemoteObjectRecord,
824 proposed: &RemoteObjectRecord,
825 owner: &StoreBatchCommitRef,
826) -> Result<RemoteObjectRecord, DbError> {
827 use coven_protocol::remote_object::{OwnedObjectState, SharedLiveSetObjectDomain};
828
829 if &existing == proposed {
830 return Ok(existing);
831 }
832 if let (
833 RemoteObjectRecord::SharedLiveSet(existing_record),
834 RemoteObjectRecord::CandidateExclusive(proposed_record),
835 ) = (&existing, proposed)
836 {
837 let proposed_owner = match &proposed_record.state {
838 coven_protocol::remote_object::CandidateObjectState::Prepared { ownership }
839 | coven_protocol::remote_object::CandidateObjectState::UploadedVerified { ownership } => {
840 ownership.pending.contains(owner)
841 }
842 coven_protocol::remote_object::CandidateObjectState::CleanupPending { .. }
843 | coven_protocol::remote_object::CandidateObjectState::AbsentVerified { .. } => false,
844 };
845 if proposed_record.identity.domain.shared_destination()
846 != Some(existing_record.identity.domain.clone())
847 || proposed_record.identity.semantic_hash != existing_record.identity.semantic_hash
848 || proposed_record.identity.object != existing_record.identity.object
849 || proposed_record.payloads != existing_record.payloads
850 || !proposed_owner
851 {
852 return Err(DbError::Message(format!(
853 "shared candidate object {} already has different identity, bytes, or ownership",
854 proposed.object_id()
855 )));
856 }
857 let mut merged = existing.clone();
858 let RemoteObjectRecord::SharedLiveSet(record) = &mut merged else {
859 unreachable!("matched shared live-set object")
860 };
861 match &mut record.state {
862 OwnedObjectState::Prepared { ownership } => {
863 ownership.pending.insert(owner.clone());
864 }
865 OwnedObjectState::UploadedVerified { ownership } => {
866 ownership.pending.insert(owner.clone());
867 }
868 OwnedObjectState::RetirementPending { former_candidates } => {
869 record.state = OwnedObjectState::UploadedVerified {
870 ownership: coven_protocol::remote_object::SharedObjectOwnership {
871 pending: BTreeSet::from([owner.clone()]),
872 activated: BTreeSet::new(),
873 nonactivated: former_candidates.clone(),
874 },
875 };
876 }
877 }
878 merged.validate().map_err(|error| {
879 DbError::context(
880 format!("merge shared candidate object {}", proposed.object_id()),
881 error,
882 )
883 })?;
884 return Ok(merged);
885 }
886 if let (
887 RemoteObjectRecord::RetainedAuthority(existing_record),
888 RemoteObjectRecord::CandidateExclusive(proposed_record),
889 ) = (&existing, proposed)
890 {
891 if proposed_record.identity.domain.retained_destination()
892 != Some(existing_record.identity.domain.clone())
893 || proposed_record.identity.semantic_hash != existing_record.identity.semantic_hash
894 || proposed_record.identity.object != existing_record.identity.object
895 || proposed_record.payloads != existing_record.payloads
896 {
897 return Err(DbError::Message(format!(
898 "retained candidate object {} already has different identity or bytes",
899 proposed.object_id()
900 )));
901 }
902 let proposed_owner = match &proposed_record.state {
903 coven_protocol::remote_object::CandidateObjectState::Prepared { ownership }
904 | coven_protocol::remote_object::CandidateObjectState::UploadedVerified { ownership } => {
905 ownership.pending.contains(owner)
906 }
907 coven_protocol::remote_object::CandidateObjectState::CleanupPending { .. }
908 | coven_protocol::remote_object::CandidateObjectState::AbsentVerified { .. } => false,
909 };
910 if !proposed_owner {
911 return Err(DbError::Message(format!(
912 "retained candidate object {} does not name its preparing commit",
913 proposed.object_id()
914 )));
915 }
916 let mut merged = existing.clone();
917 merged
918 .add_retained_authority_candidate(owner.clone())
919 .map_err(|error| {
920 DbError::context(
921 format!("merge retained candidate object {}", proposed.object_id()),
922 error,
923 )
924 })?;
925 return Ok(merged);
926 }
927 let (
928 RemoteObjectRecord::SharedLiveSet(mut existing),
929 RemoteObjectRecord::SharedLiveSet(proposed),
930 ) = (existing, proposed)
931 else {
932 return Err(DbError::Message(format!(
933 "remote object {} already has different closed state",
934 proposed.object_id()
935 )));
936 };
937 if existing.identity.domain != SharedLiveSetObjectDomain::StoredBlob
938 || proposed.identity.domain != SharedLiveSetObjectDomain::StoredBlob
939 || existing.identity != proposed.identity
940 || existing.payloads != proposed.payloads
941 {
942 return Err(DbError::Message(format!(
943 "stored blob object {} already has different identity or bytes",
944 remote_object_id(&proposed.identity.object)
945 )));
946 }
947 let proposed_has_owner = match &proposed.state {
948 OwnedObjectState::Prepared { ownership } => ownership.pending.contains(owner),
949 OwnedObjectState::UploadedVerified { ownership } => ownership.pending.contains(owner),
950 OwnedObjectState::RetirementPending { .. } => false,
951 };
952 if !proposed_has_owner {
953 return Err(DbError::Message(format!(
954 "stored blob object {} does not name its preparing commit",
955 remote_object_id(&proposed.identity.object)
956 )));
957 }
958 let proposed_uploaded = matches!(&proposed.state, OwnedObjectState::UploadedVerified { .. });
959 match &mut existing.state {
960 OwnedObjectState::Prepared { ownership } => {
961 ownership.pending.insert(owner.clone());
962 if proposed_uploaded {
963 existing.state = OwnedObjectState::UploadedVerified {
964 ownership: coven_protocol::remote_object::SharedObjectOwnership {
965 pending: ownership.pending.clone(),
966 activated: std::collections::BTreeSet::new(),
967 nonactivated: ownership.nonactivated.clone(),
968 },
969 };
970 }
971 }
972 OwnedObjectState::UploadedVerified { ownership } => {
973 ownership.pending.insert(owner.clone());
974 }
975 OwnedObjectState::RetirementPending { former_candidates } => {
976 let ownership = coven_protocol::remote_object::PendingCandidateOwnership {
977 pending: std::collections::BTreeSet::from([owner.clone()]),
978 nonactivated: former_candidates.clone(),
979 };
980 existing.state = if proposed_uploaded {
981 OwnedObjectState::UploadedVerified {
982 ownership: coven_protocol::remote_object::SharedObjectOwnership {
983 pending: ownership.pending,
984 activated: std::collections::BTreeSet::new(),
985 nonactivated: ownership.nonactivated,
986 },
987 }
988 } else {
989 OwnedObjectState::Prepared { ownership }
990 };
991 }
992 }
993 let merged = RemoteObjectRecord::SharedLiveSet(existing);
994 merged.validate().map_err(|error| {
995 DbError::context(
996 format!("merged stored blob object {}", merged.object_id()),
997 error,
998 )
999 })?;
1000 Ok(merged)
1001}
1002
1003pub(crate) fn validate_prepared_package_on(
1004 conn: &Connection,
1005 store_dir: &StoreDir,
1006 write_id: &WriteId,
1007 expected: &PreparedAudiencePackage,
1008) -> Result<(), DbError> {
1009 let audience = expected.package().audience().remote_audience();
1010 let remote_object_id: String = conn
1011 .query_row(
1012 "SELECT remote_object_id
1013 FROM store_write_packages
1014 WHERE write_id = ?1 AND audience = ?2",
1015 rusqlite::params![write_id.as_str(), remote_audience_to_db(&audience)],
1016 |row| row.get(0),
1017 )
1018 .map_err(DbError::from)?;
1019 let remote_object_id = remote_object_id
1020 .parse()
1021 .map_err(|error| DbError::context("stored prepared remote object id is invalid", error))?;
1022 let actual = PreparedAudiencePackage::from_remote(
1023 conn,
1024 store_dir,
1025 load_remote_object_on(conn, remote_object_id)?,
1026 )?;
1027 if actual.package() != expected.package()
1028 || actual.semantic_bytes() != expected.semantic_bytes()
1029 || actual.stored_bytes() != expected.stored_bytes()
1030 || actual.object() != expected.object()
1031 || actual.remote_object_id() != expected.remote_object_id()
1032 {
1033 return Err(DbError::Message(format!(
1034 "write {write_id} audience {audience:?} already has different prepared package bytes"
1035 )));
1036 }
1037 Ok(())
1038}
1039
1040pub(crate) fn validate_prepared_blob_on(
1041 conn: &Connection,
1042 write_id: &WriteId,
1043 expected: &PreparedAudienceBlob,
1044) -> Result<(), DbError> {
1045 let locator_hash = expected.blob().locator().locator_hash();
1046 let remote_object_id = expected.remote_object_id();
1047 let (stored_locator_hash, spool_path): (String, Option<String>) = conn
1048 .query_row(
1049 "SELECT locator_hash, spool_path
1050 FROM store_write_blobs
1051 WHERE write_id = ?1 AND audience = ?2 AND remote_object_id = ?3",
1052 rusqlite::params![
1053 write_id.as_str(),
1054 remote_audience_to_db(expected.audience()),
1055 remote_object_id.to_string(),
1056 ],
1057 |row| Ok((row.get(0)?, row.get(1)?)),
1058 )
1059 .map_err(DbError::from)?;
1060 if stored_locator_hash != locator_hash.to_string() {
1061 return Err(DbError::Message(format!(
1062 "write {write_id} audience {:?} exact object {remote_object_id} is indexed under locator {stored_locator_hash}, expected {locator_hash}",
1063 expected.audience()
1064 )));
1065 }
1066 let actual = PreparedAudienceBlob::from_remote(
1067 expected.audience().clone(),
1068 &locator_hash.to_string(),
1069 load_remote_object_on(conn, remote_object_id)?,
1070 spool_path.map(PathBuf::from),
1071 )?;
1072 if actual.blob() != expected.blob()
1073 || actual.spool_path() != expected.spool_path()
1074 || actual.remote_object_id() != expected.remote_object_id()
1075 {
1076 return Err(DbError::Message(format!(
1077 "write {write_id} audience {:?} exact object {remote_object_id} already has different prepared blob bytes",
1078 expected.audience()
1079 )));
1080 }
1081 Ok(())
1082}