Skip to main content

coven_database/
remote_object_records.rs

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
17/// Refuse an indexed remote object that is not the one the index names.
18///
19/// The comparison is by identity: the exact object, and the hash the record's
20/// plaintext is filed under. Payload files are named for the digest of their
21/// own contents, so a record whose semantic hash is the digest of these bytes
22/// names these bytes.
23pub(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
87/// Load one record together with the payloads it claims.
88///
89/// The row and the stored bytes are one record; flows that upload or re-encrypt an
90/// object need both halves, and reading them here keeps "the row's claims and
91/// the bytes agree" a single check rather than a per-caller convention.
92pub(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        // A stored blob is referenced by a chain: row bindings name its locator row,
365        // which names its remote object. All three leave in this transaction or none
366        // does. The bindings that remain here are stale by construction — the reclaim
367        // verified no live row resolves to this blob — and they are what the foreign
368        // keys would otherwise hold the locator row against.
369        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
417/// Install one record's payloads and record its claim on them, in the
418/// transaction that writes the row naming them.
419///
420/// The bytes land before the row commits and the claim commits with the row, so
421/// a row that exists names storage that exists. Installing them here keeps that
422/// a fact of one function instead of a per-caller convention.
423fn 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
444/// Let go of the payloads one remote object claimed, and remove its row.
445///
446/// Every deletion of a `remote_objects` row goes through here, so a payload no
447/// row names any more is owed its deletion by the same commit. Never used
448/// against a projected snapshot copy: those rows describe another device's
449/// payload storage, and deleting them must not touch this one's.
450pub(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    ) = (&current, &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                    &current_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    ) = (&current, &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                    &current_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) = &current 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 &current_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, &current)?;
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}