Skip to main content

coven_protocol/remote_object/
lifecycle.rs

1use super::identity::*;
2use super::nonactivation::*;
3use super::ownership::*;
4use super::*;
5
6/// A Store head names the commit it publishes. The record carries that commit
7/// reference, extracted from the head's bytes once when the record was built,
8/// so a load can check that the head still belongs to a candidate that owns it
9/// without parsing anything.
10fn 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    /// Everything this record asserts about itself that does not need its
36    /// payloads: where those payloads live, that the identity is the one the
37    /// record is filed under, and that its ownership state holds together.
38    ///
39    /// Byte agreement is [`Self::validate_payload`]'s job, and it is checked
40    /// where bytes arrive from outside this device's own durable state, rather
41    /// than on every load. Identity and payload cannot drift apart afterwards:
42    /// neither hash mutates across transitions, and the two domain changes that
43    /// do happen re-wrap the same reference.
44    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    /// Refuse a record whose payloads sit somewhere its domain cannot put them.
100    ///
101    /// This is what makes the carry-set structural rather than conventional: a
102    /// stored blob's row travels inside published images and carries its
103    /// locator, and no other domain may, because no other domain's payload
104    /// would arrive with the row.
105    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                // A package this device sealed uploads its own ciphertext; one
115                // it observed and activated was sealed elsewhere.
116                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    /// Check this record's identity against the plaintext it names — the whole
148    /// domain parse, its signature verifications, and its agreement with the
149    /// reference.
150    ///
151    /// Called where bytes enter from somewhere this device does not already
152    /// trust: a constructor handed the payload, a pull that parsed it off the
153    /// wire. Reading back this device's own durable state does not run it —
154    /// neither loading the row nor reading the spool file the row names, which
155    /// is named for the digest of its own contents and was fixed by this
156    /// record's identity when it was built.
157    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    /// Whether this device already created these exact bytes at the provider
337    /// and settled the create.
338    ///
339    /// The record is the evidence, so nothing that holds one needs to read the
340    /// object back to know its content: the bytes were hashed locally before
341    /// the upload and the provider's exact-upload verification settled the
342    /// create. Reading it back would test the provider's durability, not this
343    /// device's correctness, and an object that later goes missing surfaces on
344    /// the read that wants it.
345    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 &current.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}