Skip to main content

coven_protocol/remote_object/
nonactivation.rs

1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(deny_unknown_fields)]
5pub struct CandidateNonactivation {
6    candidate: StoreBatchCommitDeletionTarget,
7    pub(super) proof: CandidateNonactivationProof,
8}
9
10impl CandidateNonactivation {
11    pub fn candidate(&self) -> &StoreBatchCommitDeletionTarget {
12        &self.candidate
13    }
14
15    pub fn proof(&self) -> &CandidateNonactivationProof {
16        &self.proof
17    }
18
19    /// Checks a durable receipt shape; its caller owns the accepted boundary
20    /// or competing publication that established nonactivation.
21    pub fn validate_durable_shape(
22        candidate: &StoreBatchCommitRef,
23        commit: &crate::store_commit::StoreBatchCommit,
24        proof: CandidateNonactivationProof,
25    ) -> Result<(), RemoteObjectRecordError> {
26        let value = Self {
27            candidate: StoreBatchCommitDeletionTarget {
28                coord: candidate.coord.clone(),
29                object: candidate.object.clone(),
30                canonical_signed_bytes: commit.to_bytes(),
31            },
32            proof,
33        };
34        value.validate()
35    }
36
37    pub fn from_durable_parts(
38        candidate: &StoreBatchCommitRef,
39        commit: &crate::store_commit::StoreBatchCommit,
40        proof: CandidateNonactivationProof,
41    ) -> Result<Self, RemoteObjectRecordError> {
42        let value = Self {
43            candidate: StoreBatchCommitDeletionTarget {
44                coord: candidate.coord.clone(),
45                object: candidate.object.clone(),
46                canonical_signed_bytes: commit.to_bytes(),
47            },
48            proof,
49        };
50        value.validate()?;
51        Ok(value)
52    }
53
54    pub fn validate(&self) -> Result<(), RemoteObjectRecordError> {
55        let commit: crate::store_commit::StoreBatchCommit =
56            serde_json::from_slice(&self.candidate.canonical_signed_bytes)?;
57        if commit.seq() != self.candidate.coord.sequence() {
58            return Err(RemoteObjectRecordError::InvalidProof(
59                "candidate coordinate differs from its signed bytes".to_string(),
60            ));
61        }
62        let reference = StoreBatchCommitRef::from_commit(
63            &commit,
64            self.candidate.coord.clone(),
65            self.candidate.object.clone(),
66        )?;
67        self.proof.validate_for(&reference, &commit)
68    }
69
70    pub fn reference(&self) -> Result<StoreBatchCommitRef, RemoteObjectRecordError> {
71        let commit: crate::store_commit::StoreBatchCommit =
72            serde_json::from_slice(&self.candidate.canonical_signed_bytes)?;
73        StoreBatchCommitRef::from_commit(
74            &commit,
75            self.candidate.coord.clone(),
76            self.candidate.object.clone(),
77        )
78        .map_err(Into::into)
79    }
80
81    #[cfg(any(test, feature = "test-utils"))]
82    pub fn unverified_for_test(
83        candidate: StoreBatchCommitDeletionTarget,
84        proof: CandidateNonactivationProof,
85    ) -> Self {
86        Self { candidate, proof }
87    }
88
89    #[cfg(any(test, feature = "test-utils"))]
90    pub fn proof_mut_for_test(&mut self) -> &mut CandidateNonactivationProof {
91        &mut self.proof
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "snake_case", deny_unknown_fields)]
97pub enum CandidateNonactivationProof {
98    AcceptedAbandonment {
99        abandonment: StoreBatchCommitDeletionTarget,
100    },
101    AuthorityRetirement {
102        publication: crate::store_commit::StorePublicationRef,
103        coverage: crate::store_commit::CommitFrontier,
104        creation: crate::membership::MembershipCoord,
105        retirement: crate::membership::MembershipGrantRetirement,
106    },
107    SnapshotRetirement {
108        snapshot: crate::store_commit::AcceptedStoreSnapshotRef,
109        coverage: crate::store_commit::CommitFrontier,
110    },
111}
112
113impl CandidateNonactivationProof {
114    pub fn validate(&self) -> Result<(), RemoteObjectRecordError> {
115        match self {
116            Self::SnapshotRetirement { coverage, .. }
117            | Self::AuthorityRetirement { coverage, .. } => {
118                crate::store_commit::CommitFrontier::from_refs(
119                    coverage
120                        .commits()
121                        .iter()
122                        .map(|(stream, commit)| (stream.to_string(), commit.clone()))
123                        .collect(),
124                )?;
125                Ok(())
126            }
127            Self::AcceptedAbandonment { abandonment } => {
128                let value: crate::store_commit::StoreBatchCommit =
129                    serde_json::from_slice(&abandonment.canonical_signed_bytes)?;
130                abandonment
131                    .object
132                    .verify(&abandonment.canonical_signed_bytes)?;
133                StoreBatchCommitRef::from_commit(
134                    &value,
135                    abandonment.coord.clone(),
136                    abandonment.object.clone(),
137                )?;
138                if value.to_bytes() != abandonment.canonical_signed_bytes
139                    || value.abandoned_candidates().is_empty()
140                {
141                    return Err(RemoteObjectRecordError::InvalidProof(
142                        "accepted abandonment does not carry canonical candidate manifests".into(),
143                    ));
144                }
145                Ok(())
146            }
147        }
148    }
149
150    pub(super) fn validate_for(
151        &self,
152        candidate: &StoreBatchCommitRef,
153        commit: &crate::store_commit::StoreBatchCommit,
154    ) -> Result<(), RemoteObjectRecordError> {
155        self.validate()?;
156        match self {
157            Self::AcceptedAbandonment { abandonment } => {
158                let value: crate::store_commit::StoreBatchCommit =
159                    serde_json::from_slice(&abandonment.canonical_signed_bytes)?;
160                let target = StoreBatchCommitDeletionTarget {
161                    coord: candidate.coord.clone(),
162                    object: candidate.object.clone(),
163                    canonical_signed_bytes: commit.to_bytes(),
164                };
165                if abandonment.coord != candidate.coord
166                    || value.store_root_hash != commit.store_root_hash
167                    || value.author_registration != commit.author_registration
168                    || value.order.predecessor != commit.order.predecessor
169                    || !value
170                        .abandoned_candidates()
171                        .iter()
172                        .any(|manifest| manifest.candidate == target)
173                {
174                    return Err(RemoteObjectRecordError::InvalidProof(
175                        "accepted abandonment does not exclude the exact candidate".into(),
176                    ));
177                }
178                Ok(())
179            }
180            Self::SnapshotRetirement { snapshot, coverage } => {
181                let base = crate::store_commit::StorePublicationBase::Snapshot(snapshot.clone());
182                base.validate_for_store(commit.store_root_hash)?;
183                let later_base = match &commit.publication_base {
184                    crate::store_commit::StorePublicationBase::Genesis => true,
185                    crate::store_commit::StorePublicationBase::Snapshot(previous) => {
186                        previous.publication.position < snapshot.publication.position
187                    }
188                };
189                if !later_base
190                    || coverage
191                        .commits()
192                        .get(&candidate.coord.stream_id)
193                        .is_some_and(|covered| {
194                            covered.coord.sequence() >= candidate.coord.sequence()
195                        })
196                {
197                    return Err(RemoteObjectRecordError::InvalidProof(
198                        "snapshot does not retire an unaccepted candidate base".to_string(),
199                    ));
200                }
201                Ok(())
202            }
203            Self::AuthorityRetirement {
204                publication,
205                coverage,
206                creation,
207                ..
208            } => {
209                publication.validate_slot()?;
210                if publication.store_root_hash != commit.store_root_hash
211                    || commit.membership_authority.as_ref() != Some(creation)
212                    || coverage
213                        .commits()
214                        .get(&candidate.coord.stream_id)
215                        .is_some_and(|tip| tip.coord.sequence() >= candidate.coord.sequence())
216                {
217                    return Err(RemoteObjectRecordError::InvalidProof(
218                        "authority retirement does not bound an unaccepted candidate".into(),
219                    ));
220                }
221                Ok(())
222            }
223        }
224    }
225}
226
227pub(super) fn validate_nonactivations(
228    nonactivated: &[CandidateNonactivation],
229) -> Result<(), RemoteObjectRecordError> {
230    if nonactivated.is_empty() {
231        return Err(RemoteObjectRecordError::EmptyNonactivation);
232    }
233    let mut references = BTreeSet::new();
234    for candidate in nonactivated {
235        candidate.validate()?;
236        if !references.insert(candidate.reference()?) {
237            return Err(RemoteObjectRecordError::OverlappingOwnership);
238        }
239    }
240    Ok(())
241}
242
243pub(super) fn ensure_candidate_nonactivation(
244    former_candidates: &[CandidateNonactivation],
245    expected: &StoreBatchCommitRef,
246) -> Result<(), RemoteObjectRecordError> {
247    for candidate in former_candidates {
248        if candidate.reference()? == *expected {
249            return Ok(());
250        }
251    }
252    Err(RemoteObjectRecordError::CandidateNonactivationMissing)
253}
254
255pub(super) fn validate_owner_partition<'a>(
256    pending: &BTreeSet<StoreBatchCommitRef>,
257    activated: impl Iterator<Item = &'a StoreBatchCommitRef>,
258    nonactivated: &[CandidateNonactivation],
259) -> Result<(), RemoteObjectRecordError> {
260    let activated = activated.cloned().collect::<BTreeSet<_>>();
261    let mut former = BTreeSet::new();
262    for candidate in nonactivated {
263        candidate.validate()?;
264        former.insert(candidate.reference()?);
265    }
266    if pending
267        .iter()
268        .any(|owner| activated.contains(owner) || former.contains(owner))
269        || activated.iter().any(|owner| former.contains(owner))
270        || former.len() != nonactivated.len()
271    {
272        return Err(RemoteObjectRecordError::OverlappingOwnership);
273    }
274    Ok(())
275}
276
277pub(super) fn validate_semantic_hash(
278    expected: ObjectHash,
279    bytes: &[u8],
280) -> Result<(), RemoteObjectRecordError> {
281    let actual = ObjectHash::digest(bytes);
282    if actual != expected {
283        return Err(RemoteObjectRecordError::SemanticHashMismatch { expected, actual });
284    }
285    Ok(())
286}