Skip to main content

coven_protocol/remote_object/
nonactivation.rs

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