1use super::*;
2
3pub(super) enum UploadedRetainedNonactivation {
4 Cleanup(Vec<CandidateNonactivation>),
5 Inert(Vec<CandidateNonactivation>),
6 Retain(CandidateOwnership),
7}
8
9pub(super) fn uploaded_retained_nonactivation_disposition(
10 domain: &RetainedAuthorityObjectDomain,
11 ownership: CandidateOwnership,
12) -> UploadedRetainedNonactivation {
13 if !ownership.pending.is_empty() || !ownership.activated.is_empty() {
14 return UploadedRetainedNonactivation::Retain(ownership);
15 }
16 if matches!(
17 domain,
18 RetainedAuthorityObjectDomain::StoreMembershipResolution { .. }
19 ) {
20 UploadedRetainedNonactivation::Cleanup(ownership.nonactivated)
21 } else {
22 UploadedRetainedNonactivation::Inert(ownership.nonactivated)
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct VerifiedCandidateHeadNonactivation {
28 pub(super) candidate: StoreBatchCommitRef,
29 pub(super) head: VerifiedCandidateHead,
30}
31
32impl VerifiedCandidateHeadNonactivation {
33 pub fn head(&self) -> &VerifiedCandidateHead {
34 &self.head
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum VerifiedCandidateHead {
40 ExactCandidateAbsent { object: ExactObjectRef },
41 ExactLateCandidate { object: ExactObjectRef },
42}
43
44impl VerifiedCandidateHead {
45 pub fn object(&self) -> &ExactObjectRef {
46 match self {
47 Self::ExactCandidateAbsent { object } | Self::ExactLateCandidate { object } => object,
48 }
49 }
50}
51
52pub(super) enum CandidateHeadEvidence<'a> {
53 OccupiedByProof,
54 Verified(&'a VerifiedCandidateHeadNonactivation),
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct CandidateNonactivation {
60 candidate: StoreBatchCommitDeletionTarget,
61 pub(super) proof: CandidateNonactivationProof,
62}
63
64impl CandidateNonactivation {
65 pub fn candidate(&self) -> &StoreBatchCommitDeletionTarget {
66 &self.candidate
67 }
68
69 pub fn proof(&self) -> &CandidateNonactivationProof {
70 &self.proof
71 }
72
73 pub fn validate_durable_shape(
76 candidate: &StoreBatchCommitRef,
77 commit: &crate::store_commit::StoreBatchCommit,
78 proof: CandidateNonactivationProof,
79 ) -> Result<(), RemoteObjectRecordError> {
80 let value = Self {
81 candidate: StoreBatchCommitDeletionTarget {
82 coord: candidate.coord.clone(),
83 object: candidate.object.clone(),
84 canonical_signed_bytes: commit.to_bytes(),
85 },
86 proof,
87 };
88 value.validate()
89 }
90
91 pub fn from_durable_parts(
92 candidate: &StoreBatchCommitRef,
93 commit: &crate::store_commit::StoreBatchCommit,
94 proof: CandidateNonactivationProof,
95 ) -> Result<Self, RemoteObjectRecordError> {
96 let value = Self {
97 candidate: StoreBatchCommitDeletionTarget {
98 coord: candidate.coord.clone(),
99 object: candidate.object.clone(),
100 canonical_signed_bytes: commit.to_bytes(),
101 },
102 proof,
103 };
104 value.validate()?;
105 Ok(value)
106 }
107
108 pub fn validate(&self) -> Result<(), RemoteObjectRecordError> {
109 let commit: crate::store_commit::StoreBatchCommit =
110 serde_json::from_slice(&self.candidate.canonical_signed_bytes)?;
111 if commit.seq() != self.candidate.coord.sequence() {
112 return Err(RemoteObjectRecordError::InvalidProof(
113 "candidate coordinate differs from its signed bytes".to_string(),
114 ));
115 }
116 let reference = StoreBatchCommitRef::from_commit(
117 &commit,
118 self.candidate.coord.clone(),
119 self.candidate.object.clone(),
120 )?;
121 self.proof.validate_for(&reference, &commit)
122 }
123
124 pub fn reference(&self) -> Result<StoreBatchCommitRef, RemoteObjectRecordError> {
125 let commit: crate::store_commit::StoreBatchCommit =
126 serde_json::from_slice(&self.candidate.canonical_signed_bytes)?;
127 StoreBatchCommitRef::from_commit(
128 &commit,
129 self.candidate.coord.clone(),
130 self.candidate.object.clone(),
131 )
132 .map_err(Into::into)
133 }
134
135 #[cfg(any(test, feature = "test-utils"))]
136 pub fn unverified_for_test(
137 candidate: StoreBatchCommitDeletionTarget,
138 proof: CandidateNonactivationProof,
139 ) -> Self {
140 Self { candidate, proof }
141 }
142
143 #[cfg(any(test, feature = "test-utils"))]
144 pub fn proof_mut_for_test(&mut self) -> &mut CandidateNonactivationProof {
145 &mut self.proof
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct VerifiedCandidateNonactivation {
151 evidence: Box<VerifiedCandidateNonactivationEvidence>,
152}
153
154#[derive(Debug, Clone)]
155pub struct VerifiedDependencyRetractionAuthority {
156 durable: CandidateNonactivation,
157}
158
159impl VerifiedDependencyRetractionAuthority {
160 pub fn after_live_authority_check(
161 durable: CandidateNonactivation,
162 ) -> Result<Self, RemoteObjectRecordError> {
163 durable.validate()?;
164 if !matches!(
165 durable.proof(),
166 CandidateNonactivationProof::MergeDependencyRetraction { .. }
167 ) {
168 return Err(RemoteObjectRecordError::InvalidProof(
169 "dependent retraction authority carries another proof family".to_string(),
170 ));
171 }
172 Ok(Self { durable })
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub(super) enum VerifiedCandidateNonactivationEvidence {
178 Merge {
179 durable: CandidateNonactivation,
180 winner_commit: StoreBatchCommitRef,
181 },
182 AuthorExclusion {
183 durable: CandidateNonactivation,
184 head_nonactivation: VerifiedCandidateHeadNonactivation,
185 },
186 MembershipGrantRevocation {
187 durable: CandidateNonactivation,
188 head_nonactivation: VerifiedCandidateHeadNonactivation,
189 },
190 DependencyRetraction {
191 durable: CandidateNonactivation,
192 head_nonactivation: VerifiedCandidateHeadNonactivation,
193 },
194}
195
196impl VerifiedCandidateNonactivation {
197 pub fn from_verified_merge_winner(
198 candidate: StoreBatchCommitDeletionTarget,
199 winner_head: crate::store_commit::StoreDeviceHeadRef,
200 winner_commit: StoreBatchCommitRef,
201 ) -> Result<Self, RemoteObjectRecordError> {
202 let value = Self {
203 evidence: Box::new(VerifiedCandidateNonactivationEvidence::Merge {
204 durable: CandidateNonactivation {
205 candidate,
206 proof: CandidateNonactivationProof::MergeWinner { winner_head },
207 },
208 winner_commit,
209 }),
210 };
211 value.durable().validate()?;
212 Ok(value)
213 }
214
215 pub fn from_verified_author_exclusion(
216 durable: CandidateNonactivation,
217 candidate: StoreBatchCommitRef,
218 head: VerifiedCandidateHead,
219 ) -> Result<Self, RemoteObjectRecordError> {
220 durable.validate()?;
221 if !matches!(
222 durable.proof(),
223 CandidateNonactivationProof::AuthorExclusion { .. }
224 ) || durable.reference()? != candidate
225 {
226 return Err(RemoteObjectRecordError::InvalidProof(
227 "verified author-exclusion nonactivation parts disagree".to_string(),
228 ));
229 }
230 let value = Self {
231 evidence: Box::new(VerifiedCandidateNonactivationEvidence::AuthorExclusion {
232 durable,
233 head_nonactivation: VerifiedCandidateHeadNonactivation { candidate, head },
234 }),
235 };
236 Ok(value)
237 }
238
239 pub fn from_verified_membership_grant_revocation(
240 durable: CandidateNonactivation,
241 candidate: StoreBatchCommitRef,
242 head: VerifiedCandidateHead,
243 ) -> Result<Self, RemoteObjectRecordError> {
244 durable.validate()?;
245 if !matches!(
246 durable.proof(),
247 CandidateNonactivationProof::MergeMembershipGrantRevocation { .. }
248 ) || durable.reference()? != candidate
249 {
250 return Err(RemoteObjectRecordError::InvalidProof(
251 "verified membership-revocation nonactivation parts disagree".to_string(),
252 ));
253 }
254 let value = Self {
255 evidence: Box::new(
256 VerifiedCandidateNonactivationEvidence::MembershipGrantRevocation {
257 durable,
258 head_nonactivation: VerifiedCandidateHeadNonactivation { candidate, head },
259 },
260 ),
261 };
262 Ok(value)
263 }
264
265 pub fn dependency_retraction(
266 dependency: &Self,
267 candidate: StoreBatchCommitDeletionTarget,
268 author: &crate::store_commit::StoreDeviceRegistration,
269 activation_head_object: ExactObjectRef,
270 ) -> Result<Self, RemoteObjectRecordError> {
271 if !matches!(
272 dependency.evidence.as_ref(),
273 VerifiedCandidateNonactivationEvidence::AuthorExclusion { .. }
274 | VerifiedCandidateNonactivationEvidence::MembershipGrantRevocation { .. }
275 | VerifiedCandidateNonactivationEvidence::DependencyRetraction { .. }
276 ) {
277 return Err(RemoteObjectRecordError::InvalidProof(
278 "dependent retraction does not descend from terminal evidence".to_string(),
279 ));
280 }
281 let commit =
282 candidate.verify_nonactivation_candidate(author.store_root.store_root_hash, author)?;
283 let candidate_reference = commit.reference().clone();
284 let dependency_reference = dependency.candidate_reference()?;
285 let value = Self {
286 evidence: Box::new(
287 VerifiedCandidateNonactivationEvidence::DependencyRetraction {
288 durable: CandidateNonactivation {
289 candidate,
290 proof: CandidateNonactivationProof::MergeDependencyRetraction {
291 dependency: dependency_reference,
292 dependency_nonactivation: Box::new(dependency.durable().clone()),
293 },
294 },
295 head_nonactivation: VerifiedCandidateHeadNonactivation {
296 candidate: candidate_reference,
297 head: VerifiedCandidateHead::ExactLateCandidate {
298 object: activation_head_object,
299 },
300 },
301 },
302 ),
303 };
304 value.durable().validate()?;
305 Ok(value)
306 }
307
308 pub fn from_verified_dependency_retraction_authority(
309 authority: VerifiedDependencyRetractionAuthority,
310 candidate: StoreBatchCommitDeletionTarget,
311 author: &crate::store_commit::StoreDeviceRegistration,
312 activation_head_object: ExactObjectRef,
313 ) -> Result<Self, RemoteObjectRecordError> {
314 let commit =
315 candidate.verify_nonactivation_candidate(author.store_root.store_root_hash, author)?;
316 let candidate_reference = commit.reference().clone();
317 if authority.durable.candidate != candidate {
318 return Err(RemoteObjectRecordError::InvalidProof(
319 "verified dependent retraction authority names another candidate".to_string(),
320 ));
321 }
322 let value = Self {
323 evidence: Box::new(
324 VerifiedCandidateNonactivationEvidence::DependencyRetraction {
325 durable: authority.durable,
326 head_nonactivation: VerifiedCandidateHeadNonactivation {
327 candidate: candidate_reference,
328 head: VerifiedCandidateHead::ExactLateCandidate {
329 object: activation_head_object,
330 },
331 },
332 },
333 ),
334 };
335 value.durable().validate()?;
336 Ok(value)
337 }
338
339 pub fn candidate_reference(&self) -> Result<StoreBatchCommitRef, RemoteObjectRecordError> {
340 self.durable().reference()
341 }
342
343 pub fn proof(&self) -> &CandidateNonactivationProof {
344 &self.durable().proof
345 }
346
347 pub fn merge_winner_commit(&self) -> Result<&StoreBatchCommitRef, RemoteObjectRecordError> {
348 match self.evidence.as_ref() {
349 VerifiedCandidateNonactivationEvidence::Merge { winner_commit, .. } => {
350 Ok(winner_commit)
351 }
352 VerifiedCandidateNonactivationEvidence::AuthorExclusion { .. } => {
353 Err(RemoteObjectRecordError::InvalidProof(
354 "author-exclusion nonactivation has no Merge slot winner".to_string(),
355 ))
356 }
357 VerifiedCandidateNonactivationEvidence::MembershipGrantRevocation { .. } => {
358 Err(RemoteObjectRecordError::InvalidProof(
359 "membership-grant revocation has no Merge slot winner".to_string(),
360 ))
361 }
362 VerifiedCandidateNonactivationEvidence::DependencyRetraction { .. } => {
363 Err(RemoteObjectRecordError::InvalidProof(
364 "dependent retraction has no Merge slot winner".to_string(),
365 ))
366 }
367 }
368 }
369
370 pub fn into_durable(self) -> CandidateNonactivation {
371 match *self.evidence {
372 VerifiedCandidateNonactivationEvidence::Merge { durable, .. }
373 | VerifiedCandidateNonactivationEvidence::AuthorExclusion { durable, .. }
374 | VerifiedCandidateNonactivationEvidence::MembershipGrantRevocation {
375 durable, ..
376 }
377 | VerifiedCandidateNonactivationEvidence::DependencyRetraction { durable, .. } => {
378 durable
379 }
380 }
381 }
382
383 pub fn into_terminal_head_nonactivation(
384 self,
385 ) -> Result<(CandidateNonactivation, VerifiedCandidateHeadNonactivation), RemoteObjectRecordError>
386 {
387 match *self.evidence {
388 VerifiedCandidateNonactivationEvidence::AuthorExclusion {
389 durable,
390 head_nonactivation,
391 } => Ok((durable, head_nonactivation)),
392 VerifiedCandidateNonactivationEvidence::MembershipGrantRevocation {
393 durable,
394 head_nonactivation,
395 } => Ok((durable, head_nonactivation)),
396 VerifiedCandidateNonactivationEvidence::DependencyRetraction {
397 durable,
398 head_nonactivation,
399 } => Ok((durable, head_nonactivation)),
400 VerifiedCandidateNonactivationEvidence::Merge { .. } => {
401 Err(RemoteObjectRecordError::InvalidProof(
402 "candidate nonactivation is not verified by an excluded-author head observation"
403 .to_string(),
404 ))
405 }
406 }
407 }
408
409 fn durable(&self) -> &CandidateNonactivation {
410 match self.evidence.as_ref() {
411 VerifiedCandidateNonactivationEvidence::Merge { durable, .. }
412 | VerifiedCandidateNonactivationEvidence::AuthorExclusion { durable, .. }
413 | VerifiedCandidateNonactivationEvidence::MembershipGrantRevocation {
414 durable, ..
415 }
416 | VerifiedCandidateNonactivationEvidence::DependencyRetraction { durable, .. } => {
417 durable
418 }
419 }
420 }
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
424#[serde(rename_all = "snake_case", deny_unknown_fields)]
425pub enum CandidateNonactivationProof {
426 MergeWinner {
427 winner_head: crate::store_commit::StoreDeviceHeadRef,
428 },
429 AuthorExclusion {
430 exclusion: crate::store_commit::StoreDeviceExclusionRef,
431 accepted_cut: BTreeMap<crate::causal_grants::AuthorStreamId, StoreBatchCommitRef>,
432 activation_head: crate::store_commit::StoreDeviceHeadRef,
433 },
434 MergeMembershipGrantRevocation {
435 grant_id: crate::membership::MembershipGrantId,
436 membership: crate::circle_control::StoreMembershipStateRef,
437 activation_commit: StoreBatchCommitRef,
438 activation_head: crate::store_commit::StoreDeviceHeadRef,
439 },
440 MergeDependencyRetraction {
441 dependency: StoreBatchCommitRef,
442 dependency_nonactivation: Box<CandidateNonactivation>,
443 },
444}
445
446impl CandidateNonactivationProof {
447 pub fn validate(&self) -> Result<(), RemoteObjectRecordError> {
448 match self {
449 Self::MergeWinner { .. } => Ok(()),
450 Self::AuthorExclusion { accepted_cut, .. } => {
451 crate::store_commit::validate_store_history_cut(
452 &crate::store_commit::StoreHistoryCut::from_commits(accepted_cut.clone()),
453 )
454 .map_err(Into::into)
455 }
456 Self::MergeMembershipGrantRevocation {
457 membership,
458 activation_commit: _,
459 ..
460 } => {
461 if !membership.heads.windows(2).all(|pair| pair[0] < pair[1])
462 || !membership
463 .resolutions
464 .windows(2)
465 .all(|pair| pair[0] < pair[1])
466 {
467 return Err(RemoteObjectRecordError::InvalidProof(
468 "membership-grant revocation names a noncanonical membership state"
469 .to_string(),
470 ));
471 }
472 Ok(())
473 }
474 Self::MergeDependencyRetraction {
475 dependency,
476 dependency_nonactivation,
477 } => {
478 dependency_nonactivation.validate()?;
479 if dependency_nonactivation.reference()? != *dependency {
480 return Err(RemoteObjectRecordError::InvalidProof(
481 "dependent retraction names another exact dependency".to_string(),
482 ));
483 }
484 Ok(())
485 }
486 }
487 }
488
489 pub(super) fn validate_for(
490 &self,
491 candidate: &StoreBatchCommitRef,
492 commit: &crate::store_commit::StoreBatchCommit,
493 ) -> Result<(), RemoteObjectRecordError> {
494 self.validate()?;
495 match self {
496 Self::MergeWinner { .. } => Ok(()),
497 Self::AuthorExclusion {
498 exclusion,
499 accepted_cut,
500 ..
501 } => {
502 if commit.author_registration != exclusion.proposal.target {
503 return Err(RemoteObjectRecordError::InvalidProof(
504 "author exclusion names another candidate author or policy".to_string(),
505 ));
506 }
507 let expected_stream =
508 crate::store_commit::StreamActivation::device_authorized_stream_id(
509 commit.store_root_hash,
510 &commit.author_registration,
511 crate::store_commit::StreamAnchorDomain::StoreAnnouncements,
512 );
513 let crate::store_commit::StoreCommitCoord {
514 stream_id,
515 sequence,
516 } = candidate.coord;
517 let beyond_cutoff = match accepted_cut.get(&expected_stream) {
518 Some(reference) => sequence > reference.coord.sequence(),
519 None => true,
520 };
521 if stream_id != expected_stream || !beyond_cutoff {
522 return Err(RemoteObjectRecordError::InvalidProof(
523 "candidate is not strictly beyond its excluded author cutoff".to_string(),
524 ));
525 }
526 Ok(())
527 }
528 Self::MergeMembershipGrantRevocation { .. } => Ok(()),
529 Self::MergeDependencyRetraction { dependency, .. } => {
530 let mut direct = commit
531 .order
532 .dependencies()
533 .values()
534 .collect::<BTreeSet<_>>();
535 if let Some(predecessor) = commit.order.predecessor() {
536 direct.insert(predecessor);
537 }
538 if !direct.contains(dependency) {
539 return Err(RemoteObjectRecordError::InvalidProof(
540 "dependent retraction proof is not an exact direct dependency".to_string(),
541 ));
542 }
543 Ok(())
544 }
545 }
546 }
547}
548
549pub(super) fn validate_nonactivations(
550 nonactivated: &[CandidateNonactivation],
551) -> Result<(), RemoteObjectRecordError> {
552 if nonactivated.is_empty() {
553 return Err(RemoteObjectRecordError::EmptyNonactivation);
554 }
555 let mut references = BTreeSet::new();
556 for candidate in nonactivated {
557 candidate.validate()?;
558 if !references.insert(candidate.reference()?) {
559 return Err(RemoteObjectRecordError::OverlappingOwnership);
560 }
561 }
562 Ok(())
563}
564
565pub(super) fn ensure_candidate_nonactivation(
566 former_candidates: &[CandidateNonactivation],
567 expected: &StoreBatchCommitRef,
568) -> Result<(), RemoteObjectRecordError> {
569 for candidate in former_candidates {
570 if candidate.reference()? == *expected {
571 return Ok(());
572 }
573 }
574 Err(RemoteObjectRecordError::CandidateNonactivationMissing)
575}
576
577pub(super) fn find_nonactivation_proof<'a>(
578 former_candidates: &'a [CandidateNonactivation],
579 expected: &StoreBatchCommitRef,
580) -> Result<Option<&'a CandidateNonactivationProof>, RemoteObjectRecordError> {
581 for candidate in former_candidates {
582 if candidate.reference()? == *expected {
583 return Ok(Some(&candidate.proof));
584 }
585 }
586 Ok(None)
587}
588
589pub(super) fn validate_owner_partition<'a>(
590 pending: &BTreeSet<StoreBatchCommitRef>,
591 activated: impl Iterator<Item = &'a StoreBatchCommitRef>,
592 nonactivated: &[CandidateNonactivation],
593) -> Result<(), RemoteObjectRecordError> {
594 let activated = activated.cloned().collect::<BTreeSet<_>>();
595 let mut former = BTreeSet::new();
596 for candidate in nonactivated {
597 candidate.validate()?;
598 former.insert(candidate.reference()?);
599 }
600 if pending
601 .iter()
602 .any(|owner| activated.contains(owner) || former.contains(owner))
603 || activated.iter().any(|owner| former.contains(owner))
604 || former.len() != nonactivated.len()
605 {
606 return Err(RemoteObjectRecordError::OverlappingOwnership);
607 }
608 Ok(())
609}
610
611pub(super) fn validate_semantic_hash(
612 expected: ObjectHash,
613 bytes: &[u8],
614) -> Result<(), RemoteObjectRecordError> {
615 let actual = ObjectHash::digest(bytes);
616 if actual != expected {
617 return Err(RemoteObjectRecordError::SemanticHashMismatch { expected, actual });
618 }
619 Ok(())
620}