1use std::collections::{BTreeMap, BTreeSet};
4
5use serde::{Deserialize, Serialize};
6
7use super::circle::CircleId;
8use super::store_commit::{
9 CandidateFamilyId, CircleAckRef, ObjectHash, StoreBatchCommitRef, StreamActivationId,
10};
11use crate::objects::ExactObjectRef;
12
13mod construction;
14use nonactivation::validate_nonactivations;
15mod domains;
16mod graph;
17mod identity;
18mod lifecycle;
19mod nonactivation;
20mod ownership;
21mod pending_release;
22mod reclaim;
23
24pub use domains::{
25 CandidateExclusiveObjectDomain, CandidateExclusiveTarget, ProtocolInertObject,
26 RetainedAuthorityObjectDomain, RetainedAuthorityObjectRef, SharedLiveSetObjectDomain,
27 SharedLiveSetObjectRef,
28};
29pub use graph::{CandidateObjectGraph, CandidateObjectMaterial};
30pub use nonactivation::{CandidateNonactivation, CandidateNonactivationProof};
31pub use ownership::{
32 CandidateOwnership, OwnedObjectState, PendingCandidateOwnership, RetainedReplayOwner,
33 SharedObjectOwner, SharedObjectOwnership, SnapshotObjectOwner,
34};
35pub use pending_release::PendingCandidateRelease;
36
37const REMOTE_OBJECT_ID_DOMAIN: &[u8] = b"coven.remote-object-id.v1\0";
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case", deny_unknown_fields)]
41pub enum RemoteObjectRecord {
42 CandidateCommit(CandidateCommitRecord),
43 CandidateExclusive(CandidateObjectRecord),
44 RetainedAuthority(RetainedAuthorityRecord),
45 SharedLiveSet(SharedObjectRecord),
46}
47
48impl RemoteObjectRecord {}
49
50pub fn remote_object_id(object: &ExactObjectRef) -> ObjectHash {
51 let mut material = REMOTE_OBJECT_ID_DOMAIN.to_vec();
52 material.extend(serde_json::to_vec(object).expect("ExactObjectRef serialization cannot fail"));
53 ObjectHash::digest(&material)
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(deny_unknown_fields)]
58pub struct CandidateObjectRecord {
59 pub identity: CandidateExclusiveTarget,
60 pub payloads: RemoteObjectPayloads,
61 pub state: CandidateObjectState,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(deny_unknown_fields)]
66pub struct CandidateCommitRecord {
67 pub identity: StoreBatchCommitRef,
68 pub semantic_hash: ObjectHash,
73 pub payloads: RemoteObjectPayloads,
74 pub state: CandidateCommitState,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(deny_unknown_fields)]
79pub struct RetainedAuthorityRecord {
80 pub identity: RetainedAuthorityObjectRef,
81 pub payloads: RemoteObjectPayloads,
82 pub state: RetainedAuthorityObjectState,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case", deny_unknown_fields)]
87pub enum RetainedAuthorityObjectState {
88 Prepared {
89 ownership: PendingCandidateOwnership,
90 },
91 UploadedVerified {
92 ownership: CandidateOwnership,
93 },
94}
95
96impl RetainedAuthorityObjectState {
97 pub fn validate(&self) -> Result<(), RemoteObjectRecordError> {
98 match self {
99 Self::Prepared { ownership } => ownership.validate(),
100 Self::UploadedVerified { ownership } => ownership.validate(),
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(deny_unknown_fields)]
107pub struct SharedObjectRecord {
108 pub identity: SharedLiveSetObjectRef,
109 pub payloads: RemoteObjectPayloads,
110 pub state: OwnedObjectState,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case", deny_unknown_fields)]
125pub enum RemoteObjectPayloads {
126 SpooledInline,
129 SpooledExternal,
134 RowBlob { locator_bytes: Vec<u8> },
137}
138
139impl RemoteObjectPayloads {
140 pub fn carried_locator_bytes(&self) -> Option<&[u8]> {
143 match self {
144 Self::RowBlob { locator_bytes } => Some(locator_bytes),
145 Self::SpooledInline | Self::SpooledExternal => None,
146 }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct ClosedRemoteObject {
160 record: RemoteObjectRecord,
161 payloads: BTreeMap<ObjectHash, Vec<u8>>,
162}
163
164impl ClosedRemoteObject {
165 pub(crate) fn carried(record: RemoteObjectRecord) -> Result<Self, RemoteObjectRecordError> {
169 Self::with_payloads(record, BTreeMap::new())
170 }
171
172 fn with_spooled_payloads(
176 record: RemoteObjectRecord,
177 canonical_semantic_bytes: &[u8],
178 stored_bytes: &[u8],
179 ) -> Result<Self, RemoteObjectRecordError> {
180 let mut payloads = BTreeMap::new();
181 if let SemanticPayload::Spooled(hash) = record.semantic_payload() {
182 payloads.insert(hash, canonical_semantic_bytes.to_vec());
183 }
184 if let Some(hash) = record.stored_payload() {
185 record.object().verify(stored_bytes)?;
186 payloads.insert(hash, stored_bytes.to_vec());
187 }
188 Self::with_payloads(record, payloads)
189 }
190
191 pub fn with_payloads(
198 record: RemoteObjectRecord,
199 payloads: BTreeMap<ObjectHash, Vec<u8>>,
200 ) -> Result<Self, RemoteObjectRecordError> {
201 if payloads.keys().copied().collect::<BTreeSet<_>>() != record.payload_claims() {
202 return Err(RemoteObjectRecordError::PayloadPlacement);
203 }
204 Ok(Self { record, payloads })
205 }
206
207 pub fn record(&self) -> &RemoteObjectRecord {
208 &self.record
209 }
210
211 pub fn into_record(self) -> RemoteObjectRecord {
212 self.record
213 }
214
215 pub fn map_record(
220 self,
221 transition: impl FnOnce(
222 RemoteObjectRecord,
223 ) -> Result<RemoteObjectRecord, RemoteObjectRecordError>,
224 ) -> Result<Self, RemoteObjectRecordError> {
225 Self::with_payloads(transition(self.record)?, self.payloads)
226 }
227
228 pub fn payload_bytes(&self) -> &BTreeMap<ObjectHash, Vec<u8>> {
232 &self.payloads
233 }
234
235 pub fn semantic_bytes(&self) -> Option<&[u8]> {
239 match self.record.semantic_payload() {
240 SemanticPayload::Carried(bytes) => Some(bytes),
241 SemanticPayload::Spooled(hash) => self.payloads.get(&hash).map(Vec::as_slice),
242 SemanticPayload::Absent => None,
243 }
244 }
245
246 pub fn stored_bytes(&self) -> Option<&[u8]> {
248 self.record
249 .stored_payload()
250 .and_then(|hash| self.payloads.get(&hash).map(Vec::as_slice))
251 }
252}
253
254impl std::ops::Deref for ClosedRemoteObject {
255 type Target = RemoteObjectRecord;
256
257 fn deref(&self) -> &Self::Target {
258 &self.record
259 }
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum SemanticPayload<'record> {
267 Carried(&'record [u8]),
268 Spooled(ObjectHash),
269 Absent,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273#[serde(rename_all = "snake_case", deny_unknown_fields)]
274pub enum CandidateObjectState {
275 Prepared {
276 ownership: PendingCandidateOwnership,
277 },
278 UploadedVerified {
279 ownership: PendingCandidateOwnership,
280 },
281 CleanupPending {
282 former_candidates: Vec<CandidateNonactivation>,
283 },
284 AbsentVerified {
285 former_candidates: Vec<CandidateNonactivation>,
286 },
287}
288
289impl CandidateObjectState {
290 fn validate(&self) -> Result<(), RemoteObjectRecordError> {
291 match self {
292 Self::Prepared { ownership } | Self::UploadedVerified { ownership } => {
293 ownership.validate()
294 }
295 Self::CleanupPending { former_candidates }
296 | Self::AbsentVerified { former_candidates } => {
297 validate_nonactivations(former_candidates)
298 }
299 }
300 }
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304#[serde(rename_all = "snake_case", deny_unknown_fields)]
305pub enum CandidateCommitState {
306 Prepared,
307 UploadedVerified,
308 CleanupPending { proof: CandidateNonactivationProof },
309 AbsentVerified { proof: CandidateNonactivationProof },
310}
311
312pub use super::store_commit::StoreBatchCommitDeletionTarget;
313
314#[derive(Debug, thiserror::Error)]
315pub enum RemoteObjectRecordError {
316 #[error("prepared stored bytes do not match their exact reference: {0}")]
317 Storage(#[from] crate::objects::StorageError),
318 #[error("remote object JSON: {0}")]
319 Json(#[from] serde_json::Error),
320 #[error("remote object Store protocol: {0}")]
321 StoreProtocol(#[from] crate::store_commit::StoreProtocolError),
322 #[error("remote object audience package: {0}")]
323 AudiencePackage(#[from] crate::audience_package::AudiencePackageError),
324 #[error("remote object Circle transition: {0}")]
325 CircleTransition(#[from] crate::circle_control::CircleTransitionError),
326 #[error("remote object blob locator: {0}")]
327 BlobLocator(#[from] crate::blob::locator::BlobLocatorError),
328 #[error("remote object payload placement contradicts its domain")]
329 PayloadPlacement,
330 #[error("prepared stored reference differs from the closed identity reference")]
331 StoredReferenceMismatch,
332 #[error("prepared semantic hash is {actual}, expected {expected}")]
333 SemanticHashMismatch {
334 expected: ObjectHash,
335 actual: ObjectHash,
336 },
337 #[error("pending candidate ownership has no pending candidate")]
338 EmptyPendingOwnership,
339 #[error("candidate ownership sets overlap")]
340 OverlappingOwnership,
341 #[error("candidate ownership has no pending or activated owner")]
342 EmptyOwnership,
343 #[error("prepared canonical bytes do not parse as their claimed domain: {0}")]
344 InvalidDomain(String),
345 #[error("prepared canonical bytes disagree with their claimed domain")]
346 DomainMismatch,
347 #[error("candidate object graph contains the same exact object more than once")]
348 DuplicateCandidateObject,
349 #[error("candidate object graph material is missing")]
350 CandidateObjectMissing,
351 #[error("candidate object material is outside the signed graph")]
352 CandidateObjectInvented,
353 #[error("remote object is not uploaded for the exact activating commit")]
354 InvalidActivation,
355 #[error("remote object cannot return to uploaded state after cleanup began")]
356 InvalidUploadTransition,
357 #[error("candidate nonactivation set is empty")]
358 EmptyNonactivation,
359 #[error("candidate nonactivation proof is invalid: {0}")]
360 InvalidProof(String),
361 #[error("candidate does not own this remote object")]
362 CandidateOwnerMismatch,
363 #[error("remote object does not retain this candidate's nonactivation proof")]
364 CandidateNonactivationMissing,
365 #[error("remote object is not awaiting exact candidate cleanup")]
366 InvalidCleanupTransition,
367 #[error("remote object is not the solely-owned activated Store package being reclaimed")]
368 InvalidReclaim,
369}
370
371#[cfg(test)]
372mod tests;