1use super::identity::*;
2use super::nonactivation::*;
3use super::ownership::*;
4use super::*;
5
6fn validate_head_commit_ownership(
11 head_commit: &StoreBatchCommitRef,
12 state: &RetainedAuthorityObjectState,
13) -> Result<(), RemoteObjectRecordError> {
14 let owns_head_commit = match state {
15 RetainedAuthorityObjectState::Prepared { ownership } => {
16 ownership.pending.len() == 1 && ownership.pending.contains(head_commit)
17 }
18 RetainedAuthorityObjectState::UploadedVerified { ownership } => {
19 ownership.pending.contains(head_commit) || ownership.activated.contains(head_commit)
20 }
21 RetainedAuthorityObjectState::CleanupPending { former_candidates }
22 | RetainedAuthorityObjectState::AbsentVerified { former_candidates }
23 | RetainedAuthorityObjectState::UncreatedVerified { former_candidates } => {
24 ensure_candidate_nonactivation(former_candidates, head_commit).is_ok()
25 }
26 };
27 if owns_head_commit {
28 Ok(())
29 } else {
30 Err(RemoteObjectRecordError::CandidateOwnerMismatch)
31 }
32}
33
34impl RemoteObjectRecord {
35 pub fn validate(&self) -> Result<(), RemoteObjectRecordError> {
45 self.validate_payload_placement()?;
46 match self {
47 Self::CandidateCommit(record) => match &record.state {
48 CandidateCommitState::Prepared | CandidateCommitState::UploadedVerified => {}
49 CandidateCommitState::CleanupPending { proof }
50 | CandidateCommitState::AbsentVerified { proof } => {
51 proof.validate()?;
52 }
53 },
54 Self::CandidateExclusive(record) => {
55 if record.identity.family != record.identity.domain.family()
56 || record.identity.object != *record.identity.domain.object()
57 {
58 return Err(RemoteObjectRecordError::StoredReferenceMismatch);
59 }
60 record.state.validate()?;
61 }
62 Self::RetainedAuthority(record) => {
63 if matches!(
64 record.state,
65 RetainedAuthorityObjectState::CleanupPending { .. }
66 | RetainedAuthorityObjectState::AbsentVerified { .. }
67 ) && !matches!(
68 record.identity.domain,
69 RetainedAuthorityObjectDomain::StoreMembershipResolution { .. }
70 ) {
71 return Err(RemoteObjectRecordError::DomainMismatch);
72 }
73 if let RetainedAuthorityObjectDomain::DeviceHead { head_commit, .. } =
74 &record.identity.domain
75 {
76 validate_head_commit_ownership(head_commit, &record.state)?;
77 }
78 record.state.validate()?;
79 }
80 Self::SharedLiveSet(record) => {
81 if let SharedLiveSetObjectDomain::StoredBlob = &record.identity.domain {
82 let locator_bytes = record
83 .payloads
84 .carried_locator_bytes()
85 .ok_or(RemoteObjectRecordError::PayloadPlacement)?;
86 validate_semantic_hash(record.identity.semantic_hash, locator_bytes)?;
87 let locator = crate::blob::locator::BlobLocator::parse(locator_bytes)?;
88 crate::blob::locator::StoredBlobRef::new(
89 locator,
90 record.identity.object.clone(),
91 )?;
92 }
93 record.state.validate()?;
94 }
95 }
96 Ok(())
97 }
98
99 fn validate_payload_placement(&self) -> Result<(), RemoteObjectRecordError> {
106 let placed = match self {
107 Self::CandidateCommit(_) => {
108 matches!(self.payloads(), RemoteObjectPayloads::SpooledInline)
109 }
110 Self::CandidateExclusive(record) => match &record.identity.domain {
111 CandidateExclusiveObjectDomain::CircleBootstrapImage { .. } => {
112 matches!(record.payloads, RemoteObjectPayloads::SpooledExternal)
113 }
114 CandidateExclusiveObjectDomain::StorePackage { .. }
117 | CandidateExclusiveObjectDomain::CirclePackage { .. } => {
118 !matches!(record.payloads, RemoteObjectPayloads::RowBlob { .. })
119 }
120 _ => matches!(record.payloads, RemoteObjectPayloads::SpooledInline),
121 },
122 Self::RetainedAuthority(record) => {
123 matches!(record.payloads, RemoteObjectPayloads::SpooledInline)
124 }
125 Self::SharedLiveSet(record) => match &record.identity.domain {
126 SharedLiveSetObjectDomain::StoredBlob => {
127 matches!(record.payloads, RemoteObjectPayloads::RowBlob { .. })
128 }
129 SharedLiveSetObjectDomain::StoreSnapshotImage { .. }
130 | SharedLiveSetObjectDomain::StoreMembershipRollup { .. }
131 | SharedLiveSetObjectDomain::CircleBootstrapImage { .. } => {
132 matches!(record.payloads, RemoteObjectPayloads::SpooledExternal)
133 }
134 SharedLiveSetObjectDomain::StorePackage { .. }
135 | SharedLiveSetObjectDomain::CirclePackage { .. } => {
136 !matches!(record.payloads, RemoteObjectPayloads::RowBlob { .. })
137 }
138 },
139 };
140 if placed {
141 Ok(())
142 } else {
143 Err(RemoteObjectRecordError::PayloadPlacement)
144 }
145 }
146
147 pub fn validate_payload(
158 &self,
159 canonical_semantic_bytes: &[u8],
160 ) -> Result<(), RemoteObjectRecordError> {
161 self.validate()?;
162 match self {
163 Self::CandidateCommit(record) => {
164 validate_semantic_hash(record.semantic_hash, canonical_semantic_bytes)?;
165 let commit: crate::store_commit::StoreBatchCommit =
166 serde_json::from_slice(canonical_semantic_bytes)?;
167 record.identity.verify_commit(&commit)?;
168 match &record.state {
169 CandidateCommitState::Prepared | CandidateCommitState::UploadedVerified => {}
170 CandidateCommitState::CleanupPending { proof }
171 | CandidateCommitState::AbsentVerified { proof } => {
172 proof.validate_for(&record.identity, &commit)?;
173 }
174 }
175 }
176 Self::CandidateExclusive(record) => {
177 validate_candidate_exclusive_identity(&record.identity, canonical_semantic_bytes)?;
178 }
179 Self::RetainedAuthority(record) => {
180 validate_retained_authority_identity(&record.identity, canonical_semantic_bytes)?;
181 }
182 Self::SharedLiveSet(record) => {
183 record
184 .identity
185 .validate_semantic(canonical_semantic_bytes)?;
186 match &record.identity.domain {
187 SharedLiveSetObjectDomain::StoredBlob
188 | SharedLiveSetObjectDomain::StoreSnapshotImage { .. }
189 | SharedLiveSetObjectDomain::StoreMembershipRollup { .. }
190 | SharedLiveSetObjectDomain::CircleBootstrapImage { .. } => {}
191 SharedLiveSetObjectDomain::StorePackage { reference } => {
192 validate_package_reference(
193 reference,
194 None,
195 canonical_semantic_bytes,
196 &record.identity.object,
197 )?;
198 }
199 SharedLiveSetObjectDomain::CirclePackage { reference } => {
200 validate_package_reference(
201 &reference.package,
202 Some(reference),
203 canonical_semantic_bytes,
204 &record.identity.object,
205 )?;
206 }
207 }
208 }
209 }
210 Ok(())
211 }
212
213 pub fn into_activated(
214 self,
215 commit: &StoreBatchCommitRef,
216 ) -> Result<Self, RemoteObjectRecordError> {
217 let activated = match self {
218 Self::CandidateCommit(record) => {
219 if &record.identity != commit
220 || !matches!(record.state, CandidateCommitState::UploadedVerified)
221 {
222 return Err(RemoteObjectRecordError::InvalidActivation);
223 }
224 Self::RetainedAuthority(RetainedAuthorityRecord {
225 identity: RetainedAuthorityObjectRef {
226 semantic_hash: record.semantic_hash,
227 object: record.identity.object.clone(),
228 domain: RetainedAuthorityObjectDomain::Commit {
229 reference: record.identity,
230 },
231 },
232 payloads: record.payloads,
233 state: RetainedAuthorityObjectState::UploadedVerified {
234 ownership: CandidateOwnership {
235 pending: BTreeSet::new(),
236 activated: BTreeSet::from([commit.clone()]),
237 nonactivated: Vec::new(),
238 },
239 },
240 })
241 }
242 Self::CandidateExclusive(record) => {
243 let CandidateObjectState::UploadedVerified { ownership } = &record.state else {
244 return Err(RemoteObjectRecordError::InvalidActivation);
245 };
246 if ownership.pending.len() != 1 || !ownership.pending.contains(commit) {
247 return Err(RemoteObjectRecordError::InvalidActivation);
248 }
249 if let Some(domain) = record.identity.domain.shared_destination() {
250 Self::SharedLiveSet(SharedObjectRecord {
251 identity: SharedLiveSetObjectRef {
252 domain,
253 semantic_hash: record.identity.semantic_hash,
254 object: record.identity.object,
255 },
256 payloads: record.payloads,
257 state: OwnedObjectState::UploadedVerified {
258 ownership: SharedObjectOwnership {
259 pending: BTreeSet::new(),
260 activated: BTreeSet::from([SharedObjectOwner::StoreCommit(
261 commit.clone(),
262 )]),
263 nonactivated: Vec::new(),
264 },
265 },
266 })
267 } else if let Some(domain) = record.identity.domain.retained_destination() {
268 Self::RetainedAuthority(RetainedAuthorityRecord {
269 identity: RetainedAuthorityObjectRef {
270 domain,
271 semantic_hash: record.identity.semantic_hash,
272 object: record.identity.object,
273 },
274 payloads: record.payloads,
275 state: RetainedAuthorityObjectState::UploadedVerified {
276 ownership: CandidateOwnership {
277 pending: BTreeSet::new(),
278 activated: BTreeSet::from([commit.clone()]),
279 nonactivated: Vec::new(),
280 },
281 },
282 })
283 } else {
284 return Err(RemoteObjectRecordError::DomainMismatch);
285 }
286 }
287 Self::RetainedAuthority(mut record) => {
288 let RetainedAuthorityObjectState::UploadedVerified { ownership } =
289 &mut record.state
290 else {
291 return Err(RemoteObjectRecordError::InvalidActivation);
292 };
293 if ownership.pending.remove(commit) {
294 ownership.activated.insert(commit.clone());
295 } else if !ownership.activated.contains(commit) {
296 return Err(RemoteObjectRecordError::InvalidActivation);
297 }
298 Self::RetainedAuthority(record)
299 }
300 Self::SharedLiveSet(mut record) => {
301 match &mut record.state {
302 OwnedObjectState::UploadedVerified { ownership } => {
303 if ownership.pending.remove(commit) {
304 ownership
305 .activated
306 .insert(SharedObjectOwner::StoreCommit(commit.clone()));
307 } else if !ownership
308 .activated
309 .contains(&SharedObjectOwner::StoreCommit(commit.clone()))
310 {
311 return Err(RemoteObjectRecordError::InvalidActivation);
312 }
313 }
314 OwnedObjectState::Prepared { .. } => {
315 return Err(RemoteObjectRecordError::InvalidActivation);
316 }
317 OwnedObjectState::RetirementPending { .. } => {
318 return Err(RemoteObjectRecordError::InvalidActivation);
319 }
320 }
321 Self::SharedLiveSet(record)
322 }
323 };
324 activated.validate()?;
325 Ok(activated)
326 }
327
328 pub fn into_observed_activated(
329 mut self,
330 commit: &StoreBatchCommitRef,
331 ) -> Result<Self, RemoteObjectRecordError> {
332 self.mark_uploaded_verified()?;
333 self.into_activated(commit)
334 }
335
336 pub fn records_verified_upload(&self) -> bool {
346 match self {
347 Self::CandidateCommit(record) => {
348 matches!(record.state, CandidateCommitState::UploadedVerified)
349 }
350 Self::CandidateExclusive(record) => {
351 matches!(record.state, CandidateObjectState::UploadedVerified { .. })
352 }
353 Self::RetainedAuthority(record) => matches!(
354 record.state,
355 RetainedAuthorityObjectState::UploadedVerified { .. }
356 ),
357 Self::SharedLiveSet(record) => {
358 matches!(record.state, OwnedObjectState::UploadedVerified { .. })
359 }
360 }
361 }
362
363 pub fn mark_uploaded_verified(&mut self) -> Result<(), RemoteObjectRecordError> {
364 match self {
365 Self::CandidateCommit(record) => match record.state {
366 CandidateCommitState::Prepared => {
367 record.state = CandidateCommitState::UploadedVerified;
368 }
369 CandidateCommitState::UploadedVerified => {}
370 CandidateCommitState::CleanupPending { .. }
371 | CandidateCommitState::AbsentVerified { .. } => {
372 return Err(RemoteObjectRecordError::InvalidUploadTransition);
373 }
374 },
375 Self::CandidateExclusive(record) => match &record.state {
376 CandidateObjectState::Prepared { ownership } => {
377 record.state = CandidateObjectState::UploadedVerified {
378 ownership: ownership.clone(),
379 };
380 }
381 CandidateObjectState::UploadedVerified { .. } => {}
382 CandidateObjectState::CleanupPending { .. }
383 | CandidateObjectState::AbsentVerified { .. } => {
384 return Err(RemoteObjectRecordError::InvalidUploadTransition);
385 }
386 },
387 Self::RetainedAuthority(record) => match &record.state {
388 RetainedAuthorityObjectState::Prepared { ownership } => {
389 record.state = RetainedAuthorityObjectState::UploadedVerified {
390 ownership: CandidateOwnership {
391 pending: ownership.pending.clone(),
392 activated: BTreeSet::new(),
393 nonactivated: ownership.nonactivated.clone(),
394 },
395 };
396 }
397 RetainedAuthorityObjectState::UploadedVerified { .. } => {}
398 RetainedAuthorityObjectState::CleanupPending { .. }
399 | RetainedAuthorityObjectState::AbsentVerified { .. }
400 | RetainedAuthorityObjectState::UncreatedVerified { .. } => {
401 return Err(RemoteObjectRecordError::InvalidUploadTransition);
402 }
403 },
404 Self::SharedLiveSet(record) => match &record.state {
405 OwnedObjectState::Prepared { ownership } => {
406 record.state = OwnedObjectState::UploadedVerified {
407 ownership: SharedObjectOwnership {
408 pending: ownership.pending.clone(),
409 activated: BTreeSet::new(),
410 nonactivated: ownership.nonactivated.clone(),
411 },
412 };
413 }
414 OwnedObjectState::UploadedVerified { .. } => {}
415 OwnedObjectState::RetirementPending { .. } => {
416 return Err(RemoteObjectRecordError::InvalidUploadTransition);
417 }
418 },
419 }
420 self.validate()
421 }
422
423 pub fn add_retained_authority_candidate(
424 &mut self,
425 candidate: StoreBatchCommitRef,
426 ) -> Result<(), RemoteObjectRecordError> {
427 let Self::RetainedAuthority(record) = self else {
428 return Err(RemoteObjectRecordError::DomainMismatch);
429 };
430 let RetainedAuthorityObjectState::UploadedVerified { ownership } = &mut record.state else {
431 return Err(RemoteObjectRecordError::InvalidActivation);
432 };
433 if ownership.activated.contains(&candidate)
434 || ownership
435 .nonactivated
436 .iter()
437 .map(CandidateNonactivation::reference)
438 .collect::<Result<BTreeSet<_>, _>>()?
439 .contains(&candidate)
440 || !ownership.pending.insert(candidate)
441 {
442 return Err(RemoteObjectRecordError::OverlappingOwnership);
443 }
444 self.validate()
445 }
446
447 pub fn merge_retained_authority_activation(
448 &mut self,
449 expected: &Self,
450 owner: &StoreBatchCommitRef,
451 ) -> Result<(), RemoteObjectRecordError> {
452 let Self::RetainedAuthority(expected) = expected else {
453 return Err(RemoteObjectRecordError::DomainMismatch);
454 };
455 let RetainedAuthorityObjectState::UploadedVerified {
456 ownership: expected_ownership,
457 } = &expected.state
458 else {
459 return Err(RemoteObjectRecordError::InvalidActivation);
460 };
461 if !expected_ownership.pending.is_empty()
462 || !expected_ownership.nonactivated.is_empty()
463 || expected_ownership.activated != BTreeSet::from([owner.clone()])
464 {
465 return Err(RemoteObjectRecordError::InvalidActivation);
466 }
467 match self {
468 Self::CandidateExclusive(current) => {
469 if current.identity.domain.retained_destination()
470 != Some(expected.identity.domain.clone())
471 || current.identity.semantic_hash != expected.identity.semantic_hash
472 || current.identity.object != expected.identity.object
473 || current.payloads != expected.payloads
474 {
475 return Err(RemoteObjectRecordError::StoredReferenceMismatch);
476 }
477 let owns_activation = match ¤t.state {
478 CandidateObjectState::Prepared { ownership }
479 | CandidateObjectState::UploadedVerified { ownership } => {
480 ownership.pending == BTreeSet::from([owner.clone()])
481 }
482 CandidateObjectState::CleanupPending { .. }
483 | CandidateObjectState::AbsentVerified { .. } => false,
484 };
485 if !owns_activation {
486 return Err(RemoteObjectRecordError::InvalidActivation);
487 }
488 let mut activated = Self::CandidateExclusive(current.clone());
489 activated.mark_uploaded_verified()?;
490 *self = activated.into_activated(owner)?;
491 }
492 Self::RetainedAuthority(current) => {
493 if current.identity != expected.identity || current.payloads != expected.payloads {
494 return Err(RemoteObjectRecordError::StoredReferenceMismatch);
495 }
496 if matches!(current.state, RetainedAuthorityObjectState::Prepared { .. }) {
497 self.mark_uploaded_verified()?;
498 }
499 let Self::RetainedAuthority(current) = self else {
500 unreachable!("retained authority remains in its domain")
501 };
502 let RetainedAuthorityObjectState::UploadedVerified { ownership } =
503 &mut current.state
504 else {
505 return Err(RemoteObjectRecordError::InvalidActivation);
506 };
507 ownership.pending.remove(owner);
508 ownership.activated.insert(owner.clone());
509 }
510 Self::CandidateCommit(_) | Self::SharedLiveSet(_) => {
511 return Err(RemoteObjectRecordError::DomainMismatch);
512 }
513 }
514 self.validate()
515 }
516
517 pub fn begin_candidate_nonactivation(
518 &mut self,
519 nonactivation: CandidateNonactivation,
520 ) -> Result<Option<ProtocolInertObject>, RemoteObjectRecordError> {
521 self.begin_candidate_nonactivation_with_head_evidence(
522 nonactivation,
523 CandidateHeadEvidence::OccupiedByProof,
524 )
525 }
526
527 pub fn begin_candidate_nonactivation_with_verified_head_nonactivation(
528 &mut self,
529 nonactivation: CandidateNonactivation,
530 head_nonactivation: &VerifiedCandidateHeadNonactivation,
531 ) -> Result<Option<ProtocolInertObject>, RemoteObjectRecordError> {
532 if matches!(
533 self,
534 Self::RetainedAuthority(RetainedAuthorityRecord {
535 state: RetainedAuthorityObjectState::UncreatedVerified { .. },
536 ..
537 })
538 ) {
539 return self.reconcile_verified_candidate_head_nonactivation(
540 &nonactivation,
541 head_nonactivation,
542 );
543 }
544 self.begin_candidate_nonactivation_with_head_evidence(
545 nonactivation,
546 CandidateHeadEvidence::Verified(head_nonactivation),
547 )
548 }
549
550 fn reconcile_verified_candidate_head_nonactivation(
551 &mut self,
552 nonactivation: &CandidateNonactivation,
553 head_nonactivation: &VerifiedCandidateHeadNonactivation,
554 ) -> Result<Option<ProtocolInertObject>, RemoteObjectRecordError> {
555 nonactivation.validate()?;
556 let candidate = nonactivation.reference()?;
557 let Self::RetainedAuthority(record) = self else {
558 return Err(RemoteObjectRecordError::DomainMismatch);
559 };
560 if !matches!(
561 record.identity.domain,
562 RetainedAuthorityObjectDomain::DeviceHead { .. }
563 ) || head_nonactivation.candidate != candidate
564 || head_nonactivation.head.object() != &record.identity.object
565 {
566 return Err(RemoteObjectRecordError::InvalidProof(
567 "fresh excluded-author head evidence names another prepared object".to_string(),
568 ));
569 }
570 let RetainedAuthorityObjectState::UncreatedVerified { former_candidates } = &record.state
571 else {
572 return Err(RemoteObjectRecordError::InvalidProof(
573 "fresh excluded-author head evidence reached a nonterminal head state".to_string(),
574 ));
575 };
576 let mut stored = None;
577 for former_candidate in former_candidates {
578 if former_candidate.reference()? == candidate {
579 stored = Some(former_candidate);
580 break;
581 }
582 }
583 let stored = stored.ok_or(RemoteObjectRecordError::CandidateOwnerMismatch)?;
584 if stored != nonactivation {
585 return Err(RemoteObjectRecordError::InvalidProof(
586 "fresh excluded-author head evidence differs from its durable proof".to_string(),
587 ));
588 }
589 match &head_nonactivation.head {
590 VerifiedCandidateHead::ExactCandidateAbsent { .. } => Ok(None),
591 VerifiedCandidateHead::ExactLateCandidate { .. } => {
592 ProtocolInertObject::new(record.identity.clone(), former_candidates.clone())
593 .map(Some)
594 }
595 }
596 }
597
598 fn begin_candidate_nonactivation_with_head_evidence(
599 &mut self,
600 nonactivation: CandidateNonactivation,
601 head_evidence: CandidateHeadEvidence<'_>,
602 ) -> Result<Option<ProtocolInertObject>, RemoteObjectRecordError> {
603 nonactivation.validate()?;
604 let candidate = nonactivation.reference()?;
605 if matches!(head_evidence, CandidateHeadEvidence::Verified(_))
606 && !matches!(
607 self,
608 Self::RetainedAuthority(RetainedAuthorityRecord {
609 identity: RetainedAuthorityObjectRef {
610 domain: RetainedAuthorityObjectDomain::DeviceHead { .. },
611 ..
612 },
613 ..
614 })
615 )
616 {
617 return Err(RemoteObjectRecordError::InvalidProof(
618 "candidate head absence evidence reached a non-head object".to_string(),
619 ));
620 }
621 match self {
622 Self::CandidateCommit(record) => {
623 if record.identity != candidate {
624 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
625 }
626 match &record.state {
627 CandidateCommitState::Prepared | CandidateCommitState::UploadedVerified => {
628 record.state = CandidateCommitState::CleanupPending {
629 proof: nonactivation.proof,
630 };
631 }
632 CandidateCommitState::CleanupPending { .. }
633 | CandidateCommitState::AbsentVerified { .. } => {}
634 }
635 }
636 Self::CandidateExclusive(record) => match &mut record.state {
637 CandidateObjectState::Prepared { ownership }
638 | CandidateObjectState::UploadedVerified { ownership } => {
639 if !ownership.pending.remove(&candidate) {
640 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
641 }
642 ownership.nonactivated.push(nonactivation);
643 if ownership.pending.is_empty() {
644 record.state = CandidateObjectState::CleanupPending {
645 former_candidates: ownership.nonactivated.clone(),
646 };
647 }
648 }
649 CandidateObjectState::CleanupPending { former_candidates }
650 | CandidateObjectState::AbsentVerified { former_candidates } => {
651 ensure_candidate_nonactivation(former_candidates, &candidate)?;
652 }
653 },
654 Self::RetainedAuthority(record) => match &mut record.state {
655 RetainedAuthorityObjectState::Prepared { ownership } => {
656 let RetainedAuthorityObjectDomain::DeviceHead { .. } = &record.identity.domain
657 else {
658 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
659 };
660 match &head_evidence {
661 CandidateHeadEvidence::OccupiedByProof => {
662 let CandidateNonactivationProof::MergeWinner { winner_head } =
663 &nonactivation.proof
664 else {
665 return Err(RemoteObjectRecordError::InvalidProof(
666 "a prepared Store head requires winner or verified-absence evidence"
667 .to_string(),
668 ));
669 };
670 if winner_head.object.slot() != record.identity.object.slot()
671 || winner_head.object == record.identity.object
672 {
673 return Err(RemoteObjectRecordError::InvalidProof(
674 "Merge winner does not occupy the prepared head's exact slot"
675 .to_string(),
676 ));
677 }
678 }
679 CandidateHeadEvidence::Verified(head_nonactivation) => {
680 if !matches!(
681 nonactivation.proof,
682 CandidateNonactivationProof::AuthorExclusion { .. }
683 | CandidateNonactivationProof::MergeMembershipGrantRevocation { .. }
684 ) || head_nonactivation.candidate != candidate
685 || head_nonactivation.head.object() != &record.identity.object
686 {
687 return Err(RemoteObjectRecordError::InvalidProof(
688 "excluded-author head observation names another prepared object"
689 .to_string(),
690 ));
691 }
692 }
693 }
694 if !ownership.pending.remove(&candidate) {
695 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
696 }
697 ownership.nonactivated.push(nonactivation);
698 if ownership.pending.is_empty() {
699 match head_evidence {
700 CandidateHeadEvidence::Verified(
701 VerifiedCandidateHeadNonactivation {
702 head: VerifiedCandidateHead::ExactLateCandidate { .. },
703 ..
704 },
705 ) => {
706 return ProtocolInertObject::new(
707 record.identity.clone(),
708 ownership.nonactivated.clone(),
709 )
710 .map(Some);
711 }
712 CandidateHeadEvidence::OccupiedByProof
713 | CandidateHeadEvidence::Verified(
714 VerifiedCandidateHeadNonactivation {
715 head: VerifiedCandidateHead::ExactCandidateAbsent { .. },
716 ..
717 },
718 ) => {
719 record.state = RetainedAuthorityObjectState::UncreatedVerified {
720 former_candidates: ownership.nonactivated.clone(),
721 };
722 }
723 }
724 }
725 }
726 RetainedAuthorityObjectState::UploadedVerified { .. } => {
727 if matches!(
728 head_evidence,
729 CandidateHeadEvidence::Verified(VerifiedCandidateHeadNonactivation {
730 head: VerifiedCandidateHead::ExactCandidateAbsent { .. },
731 ..
732 })
733 ) {
734 return Err(RemoteObjectRecordError::InvalidProof(
735 "excluded-author head was verified absent but is marked uploaded"
736 .to_string(),
737 ));
738 }
739 let RetainedAuthorityObjectState::UploadedVerified { ownership } =
740 &record.state
741 else {
742 unreachable!("matched uploaded retained authority")
743 };
744 let mut ownership = ownership.clone();
745 if !ownership.pending.remove(&candidate) {
746 ensure_candidate_nonactivation(&ownership.nonactivated, &candidate)?;
747 return Ok(None);
748 }
749 ownership.nonactivated.push(nonactivation);
750 match uploaded_retained_nonactivation_disposition(
751 &record.identity.domain,
752 ownership,
753 ) {
754 UploadedRetainedNonactivation::Cleanup(former_candidates) => {
755 record.state =
756 RetainedAuthorityObjectState::CleanupPending { former_candidates };
757 }
758 UploadedRetainedNonactivation::Inert(former_candidates) => {
759 return ProtocolInertObject::new(
760 record.identity.clone(),
761 former_candidates,
762 )
763 .map(Some);
764 }
765 UploadedRetainedNonactivation::Retain(ownership) => {
766 record.state =
767 RetainedAuthorityObjectState::UploadedVerified { ownership };
768 }
769 }
770 }
771 RetainedAuthorityObjectState::UncreatedVerified { former_candidates } => {
772 if matches!(
773 head_evidence,
774 CandidateHeadEvidence::Verified(VerifiedCandidateHeadNonactivation {
775 head: VerifiedCandidateHead::ExactLateCandidate { .. },
776 ..
777 })
778 ) {
779 return Err(RemoteObjectRecordError::InvalidProof(
780 "excluded-author head is present but is marked uncreated".to_string(),
781 ));
782 }
783 ensure_candidate_nonactivation(former_candidates, &candidate)?;
784 }
785 RetainedAuthorityObjectState::CleanupPending { former_candidates }
786 | RetainedAuthorityObjectState::AbsentVerified { former_candidates } => {
787 ensure_candidate_nonactivation(former_candidates, &candidate)?;
788 }
789 },
790 Self::SharedLiveSet(record) => match &mut record.state {
791 OwnedObjectState::Prepared { ownership } => {
792 if !ownership.pending.remove(&candidate) {
793 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
794 }
795 ownership.nonactivated.push(nonactivation);
796 if ownership.pending.is_empty() {
797 record.state = OwnedObjectState::RetirementPending {
798 former_candidates: ownership.nonactivated.clone(),
799 };
800 }
801 }
802 OwnedObjectState::UploadedVerified { ownership } => {
803 if !ownership.pending.remove(&candidate) {
804 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
805 }
806 ownership.nonactivated.push(nonactivation);
807 if ownership.pending.is_empty() && ownership.activated.is_empty() {
808 record.state = OwnedObjectState::RetirementPending {
809 former_candidates: ownership.nonactivated.clone(),
810 };
811 }
812 }
813 OwnedObjectState::RetirementPending { former_candidates } => {
814 ensure_candidate_nonactivation(former_candidates, &candidate)?;
815 }
816 },
817 }
818 self.validate()?;
819 Ok(None)
820 }
821
822 pub fn cleanup_target(&self) -> Option<&ExactObjectRef> {
823 match self {
824 Self::CandidateCommit(CandidateCommitRecord {
825 state: CandidateCommitState::CleanupPending { .. },
826 ..
827 })
828 | Self::CandidateExclusive(CandidateObjectRecord {
829 state: CandidateObjectState::CleanupPending { .. },
830 ..
831 })
832 | Self::RetainedAuthority(RetainedAuthorityRecord {
833 state: RetainedAuthorityObjectState::CleanupPending { .. },
834 ..
835 }) => Some(self.object()),
836 _ => None,
837 }
838 }
839
840 pub fn mark_absent_verified(&mut self) -> Result<(), RemoteObjectRecordError> {
841 match self {
842 Self::CandidateCommit(record) => match &record.state {
843 CandidateCommitState::CleanupPending { proof } => {
844 record.state = CandidateCommitState::AbsentVerified {
845 proof: proof.clone(),
846 };
847 }
848 CandidateCommitState::AbsentVerified { .. } => {}
849 _ => return Err(RemoteObjectRecordError::InvalidCleanupTransition),
850 },
851 Self::CandidateExclusive(record) => match &record.state {
852 CandidateObjectState::CleanupPending { former_candidates } => {
853 record.state = CandidateObjectState::AbsentVerified {
854 former_candidates: former_candidates.clone(),
855 };
856 }
857 CandidateObjectState::AbsentVerified { .. } => {}
858 _ => return Err(RemoteObjectRecordError::InvalidCleanupTransition),
859 },
860 Self::RetainedAuthority(record) => match &record.state {
861 RetainedAuthorityObjectState::CleanupPending { former_candidates } => {
862 record.state = RetainedAuthorityObjectState::AbsentVerified {
863 former_candidates: former_candidates.clone(),
864 };
865 }
866 RetainedAuthorityObjectState::AbsentVerified { .. } => {}
867 _ => return Err(RemoteObjectRecordError::InvalidCleanupTransition),
868 },
869 Self::SharedLiveSet(_) => {
870 return Err(RemoteObjectRecordError::InvalidCleanupTransition)
871 }
872 }
873 self.validate()
874 }
875
876 pub fn candidate_cleanup_complete(
877 &self,
878 candidate: &StoreBatchCommitRef,
879 ) -> Result<bool, RemoteObjectRecordError> {
880 self.validate()?;
881 let contains =
882 |former: &[CandidateNonactivation]| -> Result<bool, RemoteObjectRecordError> {
883 former
884 .iter()
885 .map(CandidateNonactivation::reference)
886 .try_fold(false, |found, reference| {
887 reference.map(|reference| found || &reference == candidate)
888 })
889 };
890 match self {
891 Self::CandidateCommit(record) => Ok(&record.identity == candidate
892 && matches!(record.state, CandidateCommitState::AbsentVerified { .. })),
893 Self::CandidateExclusive(record) => match &record.state {
894 CandidateObjectState::Prepared { ownership }
895 | CandidateObjectState::UploadedVerified { ownership } => Ok(!ownership
896 .pending
897 .contains(candidate)
898 && contains(&ownership.nonactivated)?),
899 CandidateObjectState::CleanupPending { .. } => Ok(false),
900 CandidateObjectState::AbsentVerified { former_candidates } => {
901 contains(former_candidates)
902 }
903 },
904 Self::RetainedAuthority(record) => match &record.state {
905 RetainedAuthorityObjectState::Prepared { ownership } => Ok(!ownership
906 .pending
907 .contains(candidate)
908 && contains(&ownership.nonactivated)?),
909 RetainedAuthorityObjectState::UploadedVerified { ownership } => {
910 Ok(!ownership.pending.contains(candidate)
911 && !ownership.activated.contains(candidate)
912 && contains(&ownership.nonactivated)?)
913 }
914 RetainedAuthorityObjectState::CleanupPending { .. } => Ok(false),
915 RetainedAuthorityObjectState::AbsentVerified { former_candidates } => {
916 contains(former_candidates)
917 }
918 RetainedAuthorityObjectState::UncreatedVerified { former_candidates } => {
919 contains(former_candidates)
920 }
921 },
922 Self::SharedLiveSet(record) => match &record.state {
923 OwnedObjectState::Prepared { ownership } => Ok(!ownership
924 .pending
925 .contains(candidate)
926 && contains(&ownership.nonactivated)?),
927 OwnedObjectState::UploadedVerified { ownership } => {
928 Ok(!ownership.pending.contains(candidate)
929 && !ownership
930 .activated
931 .contains(&SharedObjectOwner::StoreCommit(candidate.clone()))
932 && contains(&ownership.nonactivated)?)
933 }
934 OwnedObjectState::RetirementPending { former_candidates } => {
935 contains(former_candidates)
936 }
937 },
938 }
939 }
940
941 pub fn candidate_nonactivation_proof(
942 &self,
943 candidate: &StoreBatchCommitRef,
944 ) -> Result<Option<&CandidateNonactivationProof>, RemoteObjectRecordError> {
945 self.validate()?;
946 match self {
947 Self::CandidateCommit(record) => {
948 if &record.identity != candidate {
949 return Ok(None);
950 }
951 match &record.state {
952 CandidateCommitState::CleanupPending { proof }
953 | CandidateCommitState::AbsentVerified { proof } => Ok(Some(proof)),
954 CandidateCommitState::Prepared | CandidateCommitState::UploadedVerified => {
955 Ok(None)
956 }
957 }
958 }
959 Self::CandidateExclusive(record) => match &record.state {
960 CandidateObjectState::Prepared { ownership }
961 | CandidateObjectState::UploadedVerified { ownership } => {
962 find_nonactivation_proof(&ownership.nonactivated, candidate)
963 }
964 CandidateObjectState::CleanupPending { former_candidates }
965 | CandidateObjectState::AbsentVerified { former_candidates } => {
966 find_nonactivation_proof(former_candidates, candidate)
967 }
968 },
969 Self::RetainedAuthority(record) => match &record.state {
970 RetainedAuthorityObjectState::Prepared { ownership } => {
971 find_nonactivation_proof(&ownership.nonactivated, candidate)
972 }
973 RetainedAuthorityObjectState::UploadedVerified { ownership } => {
974 find_nonactivation_proof(&ownership.nonactivated, candidate)
975 }
976 RetainedAuthorityObjectState::CleanupPending { former_candidates }
977 | RetainedAuthorityObjectState::AbsentVerified { former_candidates }
978 | RetainedAuthorityObjectState::UncreatedVerified { former_candidates } => {
979 find_nonactivation_proof(former_candidates, candidate)
980 }
981 },
982 Self::SharedLiveSet(record) => match &record.state {
983 OwnedObjectState::Prepared { ownership } => {
984 find_nonactivation_proof(&ownership.nonactivated, candidate)
985 }
986 OwnedObjectState::UploadedVerified { ownership } => {
987 find_nonactivation_proof(&ownership.nonactivated, candidate)
988 }
989 OwnedObjectState::RetirementPending { former_candidates } => {
990 find_nonactivation_proof(former_candidates, candidate)
991 }
992 },
993 }
994 }
995}