Skip to main content

coven_database/store/store_session/
candidate_records.rs

1use crate::*;
2use coven_protocol::objects::ExactObjectRef;
3use coven_protocol::remote_object::remote_object_id;
4use coven_protocol::store_commit::{ObjectHash, StoreBatchCommitRef};
5use rusqlite::Connection;
6use std::collections::BTreeSet;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct CandidateCleanupObject {
10    pub object: ExactObjectRef,
11}
12
13/// Record `nonactivation` against every object a losing candidate published, and
14/// return the ones whose durable state now names them for deletion. An object
15/// several candidates own stays until the last of them is nonactivated, so a
16/// caller can never delete an object another live candidate still needs. The
17/// candidate's own commit must reach a cleanup target: its upload may have
18/// completed before publication failed, even when the caller received no
19/// successful upload response.
20pub(crate) fn begin_candidate_nonactivation_targets_on(
21    tx: &rusqlite::Transaction<'_>,
22    candidate: &StoreBatchCommitRef,
23    objects: &[ExactObjectRef],
24    nonactivation: &coven_protocol::remote_object::CandidateNonactivation,
25) -> Result<Vec<CandidateCleanupObject>, DbError> {
26    let mut unique = BTreeSet::new();
27    let mut cleanup = Vec::new();
28    for object in objects {
29        let object_id = remote_object_id(object);
30        if !unique.insert(object_id) {
31            return Err(DbError::Message(
32                "losing candidate repeats an exact owned object".to_string(),
33            ));
34        }
35        if let Some(target) =
36            begin_remote_candidate_nonactivation_on(tx, object_id, nonactivation.clone())?
37        {
38            cleanup.push(CandidateCleanupObject { object: target });
39        }
40    }
41    let commit_complete = load_remote_object_on(tx, remote_object_id(&candidate.object))?
42        .candidate_cleanup_complete(candidate)?;
43    if !objects.contains(&candidate.object)
44        || !(commit_complete
45            || cleanup
46                .iter()
47                .any(|target| target.object == candidate.object))
48    {
49        return Err(DbError::Message(
50            "losing candidate has no exact commit cleanup target".to_string(),
51        ));
52    }
53    cleanup.sort_by(|left, right| left.object.cmp(&right.object));
54    Ok(cleanup)
55}
56
57/// The objects of an already-nonactivated candidate still awaiting deletion.
58/// Reading it again after each delete is what makes an interrupted cleanup
59/// resumable: every object has either a pending target or a completed cleanup,
60/// and anything else is a state this candidate never reached.
61pub(crate) fn candidate_cleanup_targets_on(
62    conn: &Connection,
63    candidate: &StoreBatchCommitRef,
64    objects: &[ExactObjectRef],
65) -> Result<Vec<CandidateCleanupObject>, DbError> {
66    let mut unique = BTreeSet::new();
67    let mut cleanup = Vec::new();
68    for object in objects {
69        let object_id = remote_object_id(object);
70        if !unique.insert(object_id) {
71            return Err(DbError::Message(
72                "candidate cleanup repeats an exact object".to_string(),
73            ));
74        }
75        let remote = load_remote_object_on(conn, object_id)?;
76        if let Some(target) = remote.cleanup_target() {
77            cleanup.push(CandidateCleanupObject {
78                object: target.clone(),
79            });
80        } else if !remote
81            .candidate_cleanup_complete(candidate)
82            .map_err(DbError::from)?
83        {
84            return Err(DbError::Message(format!(
85                "candidate object {object_id} has no cleanup decision"
86            )));
87        }
88    }
89    cleanup.sort_by(|left, right| left.object.cmp(&right.object));
90    Ok(cleanup)
91}
92
93pub(crate) fn require_candidate_cleanup_complete_on(
94    conn: &Connection,
95    candidate: &StoreBatchCommitRef,
96    objects: &[ExactObjectRef],
97    context: &str,
98) -> Result<(), DbError> {
99    if candidate_cleanup_targets_on(conn, candidate, objects)?.is_empty() {
100        Ok(())
101    } else {
102        Err(DbError::Message(context.to_string()))
103    }
104}
105
106pub(crate) fn delete_remote_objects_on(
107    tx: &rusqlite::Transaction<'_>,
108    object_ids: impl IntoIterator<Item = ObjectHash>,
109    context: &str,
110) -> Result<(), DbError> {
111    let mut unique = BTreeSet::new();
112    for object_id in object_ids {
113        if !unique.insert(object_id) {
114            return Err(DbError::Message(format!(
115                "{context} repeats remote object {object_id}"
116            )));
117        }
118        if !crate::remote_object_records::delete_remote_object_on(tx, object_id)? {
119            return Err(DbError::Message(format!(
120                "{context} object {object_id} disappeared during cleanup"
121            )));
122        }
123    }
124    Ok(())
125}
126
127pub(super) fn load_device_exclusion_activation_on(
128    records: crate::store::store_session::StoreRecords<'_>,
129    retained: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
130    root: &coven_protocol::store_commit::StoreRootRef,
131    exclusion: &coven_protocol::store_commit::StoreDeviceExclusionRef,
132) -> Result<StoreBatchCommitRef, DbError> {
133    let exclusion_json = serde_json::to_string(exclusion)
134        .map_err(|error| DbError::context("serialize device exclusion reference", error))?;
135    let activation_commit = records
136        .author_exclusion_activation_row(&exclusion_json)?
137        .ok_or_else(|| {
138            DbError::Message("applied device exclusion has no exact activation".into())
139        })?;
140    let activation_commit: StoreBatchCommitRef = serde_json::from_str(&activation_commit)
141        .map_err(|error| DbError::context("parse device exclusion activation commit", error))?;
142    let materialization =
143        retained.retained_materialization_by_ref_on(records, &activation_commit)?;
144    if materialization.root() != root
145        || !materialization
146            .device_operations()
147            .exclusions()
148            .any(|candidate| candidate == exclusion)
149    {
150        return Err(DbError::Message(
151            "device exclusion differs from its exact retained activation".into(),
152        ));
153    }
154    Ok(activation_commit)
155}
156
157impl super::StoreTransaction<'_, '_> {
158    pub(super) fn candidate_grant_nonactivation(
159        self,
160        authority: &mut super::verified_store_authority::VerifiedStoreAuthority,
161        membership: &coven_protocol::membership::MembershipChain,
162        candidate: &StoreBatchCommitRef,
163        commit: &coven_protocol::store_commit::StoreBatchCommit,
164        publication: &coven_protocol::store_commit::StorePublicationRef,
165    ) -> Result<coven_protocol::remote_object::CandidateNonactivation, DbError> {
166        let coverage = self.require_accepted_membership(authority, membership, publication)?;
167        let records = super::StoreRecords::new(self.transaction, self.store_dir);
168        let root = authority.required_root_authority_on(records)?;
169        let registration =
170            authority.activated_registration_on(records, &root, &commit.author_registration)?;
171        coven_protocol::store_commit::VerifiedStoreBatchCommit::parse(
172            &commit.to_bytes(),
173            root.store_root_hash,
174            candidate,
175            &registration,
176        )?;
177        let creation = commit
178            .membership_authority
179            .as_ref()
180            .ok_or_else(|| DbError::Message("candidate has no membership authority".into()))?;
181        let retirement = membership
182            .write_authority_retirement(creation, &registration.author_pubkey)
183            .ok_or_else(|| DbError::Message("candidate has no accepted grant retirement".into()))?;
184        coven_protocol::remote_object::CandidateNonactivation::from_durable_parts(
185            candidate,
186            commit,
187            coven_protocol::remote_object::CandidateNonactivationProof::AuthorityRetirement {
188                publication: publication.clone(),
189                coverage,
190                creation: creation.clone(),
191                retirement: retirement.clone(),
192            },
193        )
194        .map_err(DbError::from)
195    }
196}