coven_protocol/remote_object/
nonactivation.rs1use 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 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}