coven_protocol/remote_object/
lifecycle.rs1use super::identity::*;
2use super::nonactivation::*;
3use super::ownership::*;
4use super::*;
5
6impl RemoteObjectRecord {
7 pub fn validate(&self) -> Result<(), RemoteObjectRecordError> {
17 self.validate_payload_placement()?;
18 match self {
19 Self::CandidateCommit(record) => match &record.state {
20 CandidateCommitState::Prepared | CandidateCommitState::UploadedVerified => {}
21 CandidateCommitState::CleanupPending { proof }
22 | CandidateCommitState::AbsentVerified { proof } => {
23 proof.validate()?;
24 }
25 },
26 Self::CandidateExclusive(record) => {
27 if record.identity.family != record.identity.domain.family()
28 || record.identity.object != *record.identity.domain.object()
29 {
30 return Err(RemoteObjectRecordError::StoredReferenceMismatch);
31 }
32 record.state.validate()?;
33 }
34 Self::RetainedAuthority(record) => {
35 record.state.validate()?;
36 }
37 Self::SharedLiveSet(record) => {
38 if let SharedLiveSetObjectDomain::StoredBlob = &record.identity.domain {
39 let locator_bytes = record
40 .payloads
41 .carried_locator_bytes()
42 .ok_or(RemoteObjectRecordError::PayloadPlacement)?;
43 validate_semantic_hash(record.identity.semantic_hash, locator_bytes)?;
44 let locator = crate::blob::locator::BlobLocator::parse(locator_bytes)?;
45 crate::blob::locator::StoredBlobRef::new(
46 locator,
47 record.identity.object.clone(),
48 )?;
49 }
50 record.state.validate()?;
51 }
52 }
53 Ok(())
54 }
55
56 fn validate_payload_placement(&self) -> Result<(), RemoteObjectRecordError> {
63 let placed = match self {
64 Self::CandidateCommit(_) => {
65 matches!(self.payloads(), RemoteObjectPayloads::SpooledInline)
66 }
67 Self::CandidateExclusive(record) => match &record.identity.domain {
68 CandidateExclusiveObjectDomain::CircleBootstrapImage { .. } => {
69 matches!(record.payloads, RemoteObjectPayloads::SpooledExternal)
70 }
71 CandidateExclusiveObjectDomain::StorePackage { .. }
74 | CandidateExclusiveObjectDomain::CirclePackage { .. } => {
75 !matches!(record.payloads, RemoteObjectPayloads::RowBlob { .. })
76 }
77 _ => matches!(record.payloads, RemoteObjectPayloads::SpooledInline),
78 },
79 Self::RetainedAuthority(record) => {
80 matches!(record.payloads, RemoteObjectPayloads::SpooledInline)
81 }
82 Self::SharedLiveSet(record) => match &record.identity.domain {
83 SharedLiveSetObjectDomain::StoredBlob => {
84 matches!(record.payloads, RemoteObjectPayloads::RowBlob { .. })
85 }
86 SharedLiveSetObjectDomain::StoreSnapshotImage { .. }
87 | SharedLiveSetObjectDomain::StoreMembershipRollup { .. }
88 | SharedLiveSetObjectDomain::CircleBootstrapImage { .. } => {
89 matches!(record.payloads, RemoteObjectPayloads::SpooledExternal)
90 }
91 SharedLiveSetObjectDomain::StorePackage { .. }
92 | SharedLiveSetObjectDomain::CirclePackage { .. } => {
93 !matches!(record.payloads, RemoteObjectPayloads::RowBlob { .. })
94 }
95 },
96 };
97 if placed {
98 Ok(())
99 } else {
100 Err(RemoteObjectRecordError::PayloadPlacement)
101 }
102 }
103
104 pub fn validate_payload(
115 &self,
116 canonical_semantic_bytes: &[u8],
117 ) -> Result<(), RemoteObjectRecordError> {
118 self.validate()?;
119 match self {
120 Self::CandidateCommit(record) => {
121 validate_semantic_hash(record.semantic_hash, canonical_semantic_bytes)?;
122 let commit: crate::store_commit::StoreBatchCommit =
123 serde_json::from_slice(canonical_semantic_bytes)?;
124 record.identity.verify_commit(&commit)?;
125 match &record.state {
126 CandidateCommitState::Prepared | CandidateCommitState::UploadedVerified => {}
127 CandidateCommitState::CleanupPending { proof }
128 | CandidateCommitState::AbsentVerified { proof } => {
129 proof.validate_for(&record.identity, &commit)?;
130 }
131 }
132 }
133 Self::CandidateExclusive(record) => {
134 validate_candidate_exclusive_identity(&record.identity, canonical_semantic_bytes)?;
135 }
136 Self::RetainedAuthority(record) => {
137 validate_retained_authority_identity(&record.identity, canonical_semantic_bytes)?;
138 }
139 Self::SharedLiveSet(record) => {
140 record
141 .identity
142 .validate_semantic(canonical_semantic_bytes)?;
143 match &record.identity.domain {
144 SharedLiveSetObjectDomain::StoredBlob
145 | SharedLiveSetObjectDomain::StoreSnapshotImage { .. }
146 | SharedLiveSetObjectDomain::StoreMembershipRollup { .. }
147 | SharedLiveSetObjectDomain::CircleBootstrapImage { .. } => {}
148 SharedLiveSetObjectDomain::StorePackage { reference } => {
149 validate_package_reference(
150 reference,
151 None,
152 canonical_semantic_bytes,
153 &record.identity.object,
154 )?;
155 }
156 SharedLiveSetObjectDomain::CirclePackage { reference } => {
157 validate_package_reference(
158 &reference.package,
159 Some(reference),
160 canonical_semantic_bytes,
161 &record.identity.object,
162 )?;
163 }
164 }
165 }
166 }
167 Ok(())
168 }
169
170 pub fn into_activated(
171 self,
172 commit: &StoreBatchCommitRef,
173 ) -> Result<Self, RemoteObjectRecordError> {
174 let activated = match self {
175 Self::CandidateCommit(record) => {
176 if &record.identity != commit
177 || !matches!(record.state, CandidateCommitState::UploadedVerified)
178 {
179 return Err(RemoteObjectRecordError::InvalidActivation);
180 }
181 Self::RetainedAuthority(RetainedAuthorityRecord {
182 identity: RetainedAuthorityObjectRef {
183 semantic_hash: record.semantic_hash,
184 object: record.identity.object.clone(),
185 domain: RetainedAuthorityObjectDomain::Commit {
186 reference: record.identity,
187 },
188 },
189 payloads: record.payloads,
190 state: RetainedAuthorityObjectState::UploadedVerified {
191 ownership: CandidateOwnership {
192 pending: BTreeSet::new(),
193 activated: BTreeSet::from([commit.clone()]),
194 nonactivated: Vec::new(),
195 },
196 },
197 })
198 }
199 Self::CandidateExclusive(record) => {
200 let CandidateObjectState::UploadedVerified { ownership } = &record.state else {
201 return Err(RemoteObjectRecordError::InvalidActivation);
202 };
203 if ownership.pending.len() != 1 || !ownership.pending.contains(commit) {
204 return Err(RemoteObjectRecordError::InvalidActivation);
205 }
206 if let Some(domain) = record.identity.domain.shared_destination() {
207 Self::SharedLiveSet(SharedObjectRecord {
208 identity: SharedLiveSetObjectRef {
209 domain,
210 semantic_hash: record.identity.semantic_hash,
211 object: record.identity.object,
212 },
213 payloads: record.payloads,
214 state: OwnedObjectState::UploadedVerified {
215 ownership: SharedObjectOwnership {
216 pending: BTreeSet::new(),
217 activated: BTreeSet::from([SharedObjectOwner::StoreCommit(
218 commit.clone(),
219 )]),
220 nonactivated: Vec::new(),
221 },
222 },
223 })
224 } else if let Some(domain) = record.identity.domain.retained_destination() {
225 Self::RetainedAuthority(RetainedAuthorityRecord {
226 identity: RetainedAuthorityObjectRef {
227 domain,
228 semantic_hash: record.identity.semantic_hash,
229 object: record.identity.object,
230 },
231 payloads: record.payloads,
232 state: RetainedAuthorityObjectState::UploadedVerified {
233 ownership: CandidateOwnership {
234 pending: BTreeSet::new(),
235 activated: BTreeSet::from([commit.clone()]),
236 nonactivated: Vec::new(),
237 },
238 },
239 })
240 } else {
241 return Err(RemoteObjectRecordError::DomainMismatch);
242 }
243 }
244 Self::RetainedAuthority(mut record) => {
245 let RetainedAuthorityObjectState::UploadedVerified { ownership } =
246 &mut record.state
247 else {
248 return Err(RemoteObjectRecordError::InvalidActivation);
249 };
250 if ownership.pending.remove(commit) {
251 ownership.activated.insert(commit.clone());
252 } else if !ownership.activated.contains(commit) {
253 return Err(RemoteObjectRecordError::InvalidActivation);
254 }
255 Self::RetainedAuthority(record)
256 }
257 Self::SharedLiveSet(mut record) => {
258 match &mut record.state {
259 OwnedObjectState::UploadedVerified { ownership } => {
260 if ownership.pending.remove(commit) {
261 ownership
262 .activated
263 .insert(SharedObjectOwner::StoreCommit(commit.clone()));
264 } else if !ownership
265 .activated
266 .contains(&SharedObjectOwner::StoreCommit(commit.clone()))
267 {
268 return Err(RemoteObjectRecordError::InvalidActivation);
269 }
270 }
271 OwnedObjectState::Prepared { .. } => {
272 return Err(RemoteObjectRecordError::InvalidActivation);
273 }
274 OwnedObjectState::RetirementPending { .. } => {
275 return Err(RemoteObjectRecordError::InvalidActivation);
276 }
277 }
278 Self::SharedLiveSet(record)
279 }
280 };
281 activated.validate()?;
282 Ok(activated)
283 }
284
285 pub fn into_observed_activated(
286 mut self,
287 commit: &StoreBatchCommitRef,
288 ) -> Result<Self, RemoteObjectRecordError> {
289 self.mark_uploaded_verified()?;
290 self.into_activated(commit)
291 }
292
293 pub fn records_verified_upload(&self) -> bool {
303 match self {
304 Self::CandidateCommit(record) => {
305 matches!(record.state, CandidateCommitState::UploadedVerified)
306 }
307 Self::CandidateExclusive(record) => {
308 matches!(record.state, CandidateObjectState::UploadedVerified { .. })
309 }
310 Self::RetainedAuthority(record) => matches!(
311 record.state,
312 RetainedAuthorityObjectState::UploadedVerified { .. }
313 ),
314 Self::SharedLiveSet(record) => {
315 matches!(record.state, OwnedObjectState::UploadedVerified { .. })
316 }
317 }
318 }
319
320 pub fn mark_uploaded_verified(&mut self) -> Result<(), RemoteObjectRecordError> {
321 match self {
322 Self::CandidateCommit(record) => match record.state {
323 CandidateCommitState::Prepared => {
324 record.state = CandidateCommitState::UploadedVerified;
325 }
326 CandidateCommitState::UploadedVerified => {}
327 CandidateCommitState::CleanupPending { .. }
328 | CandidateCommitState::AbsentVerified { .. } => {
329 return Err(RemoteObjectRecordError::InvalidUploadTransition);
330 }
331 },
332 Self::CandidateExclusive(record) => match &record.state {
333 CandidateObjectState::Prepared { ownership } => {
334 record.state = CandidateObjectState::UploadedVerified {
335 ownership: ownership.clone(),
336 };
337 }
338 CandidateObjectState::UploadedVerified { .. } => {}
339 CandidateObjectState::CleanupPending { .. }
340 | CandidateObjectState::AbsentVerified { .. } => {
341 return Err(RemoteObjectRecordError::InvalidUploadTransition);
342 }
343 },
344 Self::RetainedAuthority(record) => match &record.state {
345 RetainedAuthorityObjectState::Prepared { ownership } => {
346 record.state = RetainedAuthorityObjectState::UploadedVerified {
347 ownership: CandidateOwnership {
348 pending: ownership.pending.clone(),
349 activated: BTreeSet::new(),
350 nonactivated: ownership.nonactivated.clone(),
351 },
352 };
353 }
354 RetainedAuthorityObjectState::UploadedVerified { .. } => {}
355 },
356 Self::SharedLiveSet(record) => match &record.state {
357 OwnedObjectState::Prepared { ownership } => {
358 record.state = OwnedObjectState::UploadedVerified {
359 ownership: SharedObjectOwnership {
360 pending: ownership.pending.clone(),
361 activated: BTreeSet::new(),
362 nonactivated: ownership.nonactivated.clone(),
363 },
364 };
365 }
366 OwnedObjectState::UploadedVerified { .. } => {}
367 OwnedObjectState::RetirementPending { .. } => {
368 return Err(RemoteObjectRecordError::InvalidUploadTransition);
369 }
370 },
371 }
372 self.validate()
373 }
374
375 pub fn add_retained_authority_candidate(
376 &mut self,
377 candidate: StoreBatchCommitRef,
378 ) -> Result<(), RemoteObjectRecordError> {
379 let Self::RetainedAuthority(record) = self else {
380 return Err(RemoteObjectRecordError::DomainMismatch);
381 };
382 let RetainedAuthorityObjectState::UploadedVerified { ownership } = &mut record.state else {
383 return Err(RemoteObjectRecordError::InvalidActivation);
384 };
385 if ownership.activated.contains(&candidate)
386 || ownership
387 .nonactivated
388 .iter()
389 .map(CandidateNonactivation::reference)
390 .collect::<Result<BTreeSet<_>, _>>()?
391 .contains(&candidate)
392 || !ownership.pending.insert(candidate)
393 {
394 return Err(RemoteObjectRecordError::OverlappingOwnership);
395 }
396 self.validate()
397 }
398
399 pub fn merge_retained_authority_activation(
400 &mut self,
401 expected: &Self,
402 owner: &StoreBatchCommitRef,
403 ) -> Result<(), RemoteObjectRecordError> {
404 let Self::RetainedAuthority(expected) = expected else {
405 return Err(RemoteObjectRecordError::DomainMismatch);
406 };
407 let RetainedAuthorityObjectState::UploadedVerified {
408 ownership: expected_ownership,
409 } = &expected.state
410 else {
411 return Err(RemoteObjectRecordError::InvalidActivation);
412 };
413 if !expected_ownership.pending.is_empty()
414 || !expected_ownership.nonactivated.is_empty()
415 || expected_ownership.activated != BTreeSet::from([owner.clone()])
416 {
417 return Err(RemoteObjectRecordError::InvalidActivation);
418 }
419 match self {
420 Self::CandidateExclusive(current) => {
421 if current.identity.domain.retained_destination()
422 != Some(expected.identity.domain.clone())
423 || current.identity.semantic_hash != expected.identity.semantic_hash
424 || current.identity.object != expected.identity.object
425 || current.payloads != expected.payloads
426 {
427 return Err(RemoteObjectRecordError::StoredReferenceMismatch);
428 }
429 let owns_activation = match ¤t.state {
430 CandidateObjectState::Prepared { ownership }
431 | CandidateObjectState::UploadedVerified { ownership } => {
432 ownership.pending == BTreeSet::from([owner.clone()])
433 }
434 CandidateObjectState::CleanupPending { .. }
435 | CandidateObjectState::AbsentVerified { .. } => false,
436 };
437 if !owns_activation {
438 return Err(RemoteObjectRecordError::InvalidActivation);
439 }
440 let mut activated = Self::CandidateExclusive(current.clone());
441 activated.mark_uploaded_verified()?;
442 *self = activated.into_activated(owner)?;
443 }
444 Self::RetainedAuthority(current) => {
445 if current.identity != expected.identity || current.payloads != expected.payloads {
446 return Err(RemoteObjectRecordError::StoredReferenceMismatch);
447 }
448 if matches!(current.state, RetainedAuthorityObjectState::Prepared { .. }) {
449 self.mark_uploaded_verified()?;
450 }
451 let Self::RetainedAuthority(current) = self else {
452 unreachable!("retained authority remains in its domain")
453 };
454 let RetainedAuthorityObjectState::UploadedVerified { ownership } =
455 &mut current.state
456 else {
457 return Err(RemoteObjectRecordError::InvalidActivation);
458 };
459 ownership.pending.remove(owner);
460 ownership.activated.insert(owner.clone());
461 }
462 Self::CandidateCommit(_) | Self::SharedLiveSet(_) => {
463 return Err(RemoteObjectRecordError::DomainMismatch);
464 }
465 }
466 self.validate()
467 }
468
469 pub fn begin_candidate_nonactivation(
470 &mut self,
471 nonactivation: CandidateNonactivation,
472 ) -> Result<Option<ProtocolInertObject>, RemoteObjectRecordError> {
473 nonactivation.validate()?;
474 let candidate = nonactivation.reference()?;
475 match self {
476 Self::CandidateCommit(record) => {
477 if record.identity != candidate {
478 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
479 }
480 match &record.state {
481 CandidateCommitState::Prepared | CandidateCommitState::UploadedVerified => {
482 record.state = CandidateCommitState::CleanupPending {
483 proof: nonactivation.proof,
484 };
485 }
486 CandidateCommitState::CleanupPending { .. }
487 | CandidateCommitState::AbsentVerified { .. } => {}
488 }
489 }
490 Self::CandidateExclusive(record) => match &mut record.state {
491 CandidateObjectState::Prepared { ownership }
492 | CandidateObjectState::UploadedVerified { ownership } => {
493 if !ownership.pending.remove(&candidate) {
494 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
495 }
496 ownership.nonactivated.push(nonactivation);
497 if ownership.pending.is_empty() {
498 record.state = CandidateObjectState::CleanupPending {
499 former_candidates: ownership.nonactivated.clone(),
500 };
501 }
502 }
503 CandidateObjectState::CleanupPending { former_candidates }
504 | CandidateObjectState::AbsentVerified { former_candidates } => {
505 ensure_candidate_nonactivation(former_candidates, &candidate)?;
506 }
507 },
508 Self::RetainedAuthority(record) => match &mut record.state {
509 RetainedAuthorityObjectState::Prepared { .. } => {
510 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
511 }
512 RetainedAuthorityObjectState::UploadedVerified { .. } => {
513 let RetainedAuthorityObjectState::UploadedVerified { ownership } =
514 &record.state
515 else {
516 unreachable!("matched uploaded retained authority")
517 };
518 let mut ownership = ownership.clone();
519 if !ownership.pending.remove(&candidate) {
520 ensure_candidate_nonactivation(&ownership.nonactivated, &candidate)?;
521 return Ok(None);
522 }
523 ownership.nonactivated.push(nonactivation);
524 if ownership.pending.is_empty() && ownership.activated.is_empty() {
525 return ProtocolInertObject::new(
526 record.identity.clone(),
527 ownership.nonactivated,
528 )
529 .map(Some);
530 }
531 record.state = RetainedAuthorityObjectState::UploadedVerified { ownership };
532 }
533 },
534 Self::SharedLiveSet(record) => match &mut record.state {
535 OwnedObjectState::Prepared { ownership } => {
536 if !ownership.pending.remove(&candidate) {
537 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
538 }
539 ownership.nonactivated.push(nonactivation);
540 if ownership.pending.is_empty() {
541 record.state = OwnedObjectState::RetirementPending {
542 former_candidates: ownership.nonactivated.clone(),
543 };
544 }
545 }
546 OwnedObjectState::UploadedVerified { ownership } => {
547 if !ownership.pending.remove(&candidate) {
548 return Err(RemoteObjectRecordError::CandidateOwnerMismatch);
549 }
550 ownership.nonactivated.push(nonactivation);
551 if ownership.pending.is_empty() && ownership.activated.is_empty() {
552 record.state = OwnedObjectState::RetirementPending {
553 former_candidates: ownership.nonactivated.clone(),
554 };
555 }
556 }
557 OwnedObjectState::RetirementPending { former_candidates } => {
558 ensure_candidate_nonactivation(former_candidates, &candidate)?;
559 }
560 },
561 }
562 self.validate()?;
563 Ok(None)
564 }
565
566 pub fn cleanup_target(&self) -> Option<&ExactObjectRef> {
567 match self {
568 Self::CandidateCommit(CandidateCommitRecord {
569 state: CandidateCommitState::CleanupPending { .. },
570 ..
571 })
572 | Self::CandidateExclusive(CandidateObjectRecord {
573 state: CandidateObjectState::CleanupPending { .. },
574 ..
575 }) => Some(self.object()),
576 _ => None,
577 }
578 }
579
580 pub fn mark_absent_verified(&mut self) -> Result<(), RemoteObjectRecordError> {
581 match self {
582 Self::CandidateCommit(record) => match &record.state {
583 CandidateCommitState::CleanupPending { proof } => {
584 record.state = CandidateCommitState::AbsentVerified {
585 proof: proof.clone(),
586 };
587 }
588 CandidateCommitState::AbsentVerified { .. } => {}
589 _ => return Err(RemoteObjectRecordError::InvalidCleanupTransition),
590 },
591 Self::CandidateExclusive(record) => match &record.state {
592 CandidateObjectState::CleanupPending { former_candidates } => {
593 record.state = CandidateObjectState::AbsentVerified {
594 former_candidates: former_candidates.clone(),
595 };
596 }
597 CandidateObjectState::AbsentVerified { .. } => {}
598 _ => return Err(RemoteObjectRecordError::InvalidCleanupTransition),
599 },
600 Self::RetainedAuthority(_) | Self::SharedLiveSet(_) => {
601 return Err(RemoteObjectRecordError::InvalidCleanupTransition);
602 }
603 }
604 self.validate()
605 }
606
607 pub fn candidate_cleanup_complete(
608 &self,
609 candidate: &StoreBatchCommitRef,
610 ) -> Result<bool, RemoteObjectRecordError> {
611 self.validate()?;
612 let contains =
613 |former: &[CandidateNonactivation]| -> Result<bool, RemoteObjectRecordError> {
614 former
615 .iter()
616 .map(CandidateNonactivation::reference)
617 .try_fold(false, |found, reference| {
618 reference.map(|reference| found || &reference == candidate)
619 })
620 };
621 match self {
622 Self::CandidateCommit(record) => Ok(&record.identity == candidate
623 && matches!(record.state, CandidateCommitState::AbsentVerified { .. })),
624 Self::CandidateExclusive(record) => match &record.state {
625 CandidateObjectState::Prepared { ownership }
626 | CandidateObjectState::UploadedVerified { ownership } => Ok(!ownership
627 .pending
628 .contains(candidate)
629 && contains(&ownership.nonactivated)?),
630 CandidateObjectState::CleanupPending { .. } => Ok(false),
631 CandidateObjectState::AbsentVerified { former_candidates } => {
632 contains(former_candidates)
633 }
634 },
635 Self::RetainedAuthority(record) => match &record.state {
636 RetainedAuthorityObjectState::Prepared { ownership } => Ok(!ownership
637 .pending
638 .contains(candidate)
639 && contains(&ownership.nonactivated)?),
640 RetainedAuthorityObjectState::UploadedVerified { ownership } => {
641 Ok(!ownership.pending.contains(candidate)
642 && !ownership.activated.contains(candidate)
643 && contains(&ownership.nonactivated)?)
644 }
645 },
646 Self::SharedLiveSet(record) => match &record.state {
647 OwnedObjectState::Prepared { ownership } => Ok(!ownership
648 .pending
649 .contains(candidate)
650 && contains(&ownership.nonactivated)?),
651 OwnedObjectState::UploadedVerified { ownership } => {
652 Ok(!ownership.pending.contains(candidate)
653 && !ownership
654 .activated
655 .contains(&SharedObjectOwner::StoreCommit(candidate.clone()))
656 && contains(&ownership.nonactivated)?)
657 }
658 OwnedObjectState::RetirementPending { former_candidates } => {
659 contains(former_candidates)
660 }
661 },
662 }
663 }
664}