Skip to main content

coven_protocol/remote_object/
graph.rs

1use super::identity::*;
2use super::*;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct CandidateObjectMaterial {
6    pub object: ExactObjectRef,
7    pub canonical_semantic_bytes: Vec<u8>,
8    /// The ciphertext this object is uploaded as. Carried here because the
9    /// transaction that writes the record's row is what installs it in the
10    /// payload spool, and a record cannot be persisted without it.
11    pub stored_bytes: Vec<u8>,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct CandidateObjectGraph {
16    family: CandidateFamilyId,
17    objects: Vec<CandidateExclusiveObjectDomain>,
18}
19
20impl CandidateObjectGraph {
21    pub fn from_commit(
22        commit: &crate::store_commit::StoreBatchCommit,
23    ) -> Result<Self, RemoteObjectRecordError> {
24        let manifest = commit.verified_candidate_objects()?;
25        let mut objects = Vec::new();
26        for candidate in &manifest.objects {
27            match candidate {
28                crate::store_commit::CandidateExclusiveObjectRef::StorePackage(reference) => {
29                    objects.push(CandidateExclusiveObjectDomain::StorePackage {
30                        reference: reference.clone(),
31                    });
32                }
33                crate::store_commit::CandidateExclusiveObjectRef::CirclePackage(reference) => {
34                    objects.push(CandidateExclusiveObjectDomain::CirclePackage {
35                        reference: reference.clone(),
36                    });
37                }
38                crate::store_commit::CandidateExclusiveObjectRef::CircleAccess {
39                    circle_id,
40                    access,
41                } => {
42                    objects.push(CandidateExclusiveObjectDomain::CircleAccessLeaf {
43                        family: manifest.family,
44                        circle_id: *circle_id,
45                        reference: access.leaf.clone(),
46                    });
47                    objects.push(CandidateExclusiveObjectDomain::CircleAccessEnvelope {
48                        family: manifest.family,
49                        circle_id: *circle_id,
50                        reference: access.envelope.clone(),
51                    });
52                    if let Some(bootstrap) = &access.bootstrap {
53                        objects.push(CandidateExclusiveObjectDomain::CircleBootstrapImage {
54                            family: manifest.family,
55                            circle_id: *circle_id,
56                            owner_pubkey: access.leaf.owner_pubkey.clone(),
57                            epoch_id: access.leaf.epoch_id,
58                            recipient_slot: access.leaf.recipient_slot.clone(),
59                            reference: bootstrap.clone(),
60                        });
61                    }
62                }
63                crate::store_commit::CandidateExclusiveObjectRef::CircleEpochCloseIntent {
64                    circle_id,
65                    reference,
66                } => {
67                    objects.push(CandidateExclusiveObjectDomain::CircleEpochCloseIntent {
68                        family: manifest.family,
69                        circle_id: *circle_id,
70                        reference: reference.clone(),
71                    });
72                }
73                crate::store_commit::CandidateExclusiveObjectRef::CircleEpochCloseOutcome {
74                    circle_id,
75                    reference,
76                } => {
77                    objects.push(CandidateExclusiveObjectDomain::CircleEpochCloseOutcome {
78                        family: manifest.family,
79                        circle_id: *circle_id,
80                        reference: reference.clone(),
81                    });
82                }
83                crate::store_commit::CandidateExclusiveObjectRef::CircleEpochCloseCancellation {
84                    circle_id,
85                    reference,
86                } => {
87                    objects.push(CandidateExclusiveObjectDomain::CircleEpochCloseCancellation {
88                        family: manifest.family,
89                        circle_id: *circle_id,
90                        reference: reference.clone(),
91                    });
92                }
93            }
94        }
95        Ok(Self {
96            family: manifest.family,
97            objects,
98        })
99    }
100
101    pub fn exact_objects(&self) -> impl Iterator<Item = &ExactObjectRef> {
102        self.objects
103            .iter()
104            .map(CandidateExclusiveObjectDomain::object)
105    }
106
107    pub fn close(
108        self,
109        commit: &crate::store_commit::StoreBatchCommit,
110        owner: &StoreBatchCommitRef,
111        materials: Vec<CandidateObjectMaterial>,
112    ) -> Result<Vec<ClosedRemoteObject>, RemoteObjectRecordError> {
113        owner.verify_commit(commit)?;
114        if self.family != commit.candidate_family() {
115            return Err(RemoteObjectRecordError::DomainMismatch);
116        }
117        let mut exact = std::collections::BTreeMap::new();
118        for material in materials {
119            if exact.insert(material.object.clone(), material).is_some() {
120                return Err(RemoteObjectRecordError::DuplicateCandidateObject);
121            }
122        }
123        validate_access_pairs(&self.objects, &exact)?;
124        let mut records = Vec::with_capacity(self.objects.len());
125        for domain in self.objects {
126            let object = domain.object().clone();
127            let material = exact
128                .remove(&object)
129                .ok_or(RemoteObjectRecordError::CandidateObjectMissing)?;
130            let (semantic_hash, payloads) = match &domain {
131                CandidateExclusiveObjectDomain::CircleBootstrapImage { reference, .. } => {
132                    (reference.image_hash, RemoteObjectPayloads::SpooledExternal)
133                }
134                _ => (
135                    ObjectHash::digest(&material.canonical_semantic_bytes),
136                    RemoteObjectPayloads::SpooledInline,
137                ),
138            };
139            let record = RemoteObjectRecord::CandidateExclusive(CandidateObjectRecord {
140                identity: CandidateExclusiveTarget {
141                    family: domain.family(),
142                    domain,
143                    semantic_hash,
144                    object,
145                },
146                payloads,
147                state: CandidateObjectState::Prepared {
148                    ownership: PendingCandidateOwnership {
149                        pending: BTreeSet::from([owner.clone()]),
150                        nonactivated: Vec::new(),
151                    },
152                },
153            });
154            record.validate_payload(&material.canonical_semantic_bytes)?;
155            records.push(ClosedRemoteObject::with_spooled_payloads(
156                record,
157                &material.canonical_semantic_bytes,
158                &material.stored_bytes,
159            )?);
160        }
161        if !exact.is_empty() {
162            return Err(RemoteObjectRecordError::CandidateObjectInvented);
163        }
164        Ok(records)
165    }
166}