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 reclaim;
22
23pub use domains::{
24 CandidateExclusiveObjectDomain, CandidateExclusiveTarget, ProtocolInertObject,
25 RetainedAuthorityObjectDomain, RetainedAuthorityObjectRef, SharedLiveSetObjectDomain,
26 SharedLiveSetObjectRef,
27};
28pub use graph::{CandidateObjectGraph, CandidateObjectMaterial};
29pub use nonactivation::{
30 CandidateNonactivation, CandidateNonactivationProof, VerifiedCandidateHead,
31 VerifiedCandidateHeadNonactivation, VerifiedCandidateNonactivation,
32 VerifiedDependencyRetractionAuthority,
33};
34pub use ownership::{
35 CandidateOwnership, OwnedObjectState, PendingCandidateOwnership, RetainedReplayOwner,
36 SharedObjectOwner, SharedObjectOwnership, SnapshotObjectOwner,
37};
38
39const REMOTE_OBJECT_ID_DOMAIN: &[u8] = b"coven.remote-object-id.v1\0";
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case", deny_unknown_fields)]
43pub enum RemoteObjectRecord {
44 CandidateCommit(CandidateCommitRecord),
45 CandidateExclusive(CandidateObjectRecord),
46 RetainedAuthority(RetainedAuthorityRecord),
47 SharedLiveSet(SharedObjectRecord),
48}
49
50impl RemoteObjectRecord {}
51
52pub fn remote_object_id(object: &ExactObjectRef) -> ObjectHash {
53 let mut material = REMOTE_OBJECT_ID_DOMAIN.to_vec();
54 material.extend(serde_json::to_vec(object).expect("ExactObjectRef serialization cannot fail"));
55 ObjectHash::digest(&material)
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct CandidateObjectRecord {
61 pub identity: CandidateExclusiveTarget,
62 pub payloads: RemoteObjectPayloads,
63 pub state: CandidateObjectState,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68pub struct CandidateCommitRecord {
69 pub identity: StoreBatchCommitRef,
70 pub semantic_hash: ObjectHash,
75 pub payloads: RemoteObjectPayloads,
76 pub state: CandidateCommitState,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(deny_unknown_fields)]
81pub struct RetainedAuthorityRecord {
82 pub identity: RetainedAuthorityObjectRef,
83 pub payloads: RemoteObjectPayloads,
84 pub state: RetainedAuthorityObjectState,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case", deny_unknown_fields)]
89pub enum RetainedAuthorityObjectState {
90 Prepared {
91 ownership: PendingCandidateOwnership,
92 },
93 UploadedVerified {
94 ownership: CandidateOwnership,
95 },
96 CleanupPending {
97 former_candidates: Vec<CandidateNonactivation>,
98 },
99 AbsentVerified {
100 former_candidates: Vec<CandidateNonactivation>,
101 },
102 UncreatedVerified {
103 former_candidates: Vec<CandidateNonactivation>,
104 },
105}
106
107impl RetainedAuthorityObjectState {
108 pub fn validate(&self) -> Result<(), RemoteObjectRecordError> {
109 match self {
110 Self::Prepared { ownership } => ownership.validate(),
111 Self::UploadedVerified { ownership } => ownership.validate(),
112 Self::CleanupPending { former_candidates }
113 | Self::AbsentVerified { former_candidates }
114 | Self::UncreatedVerified { former_candidates } => {
115 validate_nonactivations(former_candidates)
116 }
117 }
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(deny_unknown_fields)]
123pub struct SharedObjectRecord {
124 pub identity: SharedLiveSetObjectRef,
125 pub payloads: RemoteObjectPayloads,
126 pub state: OwnedObjectState,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "snake_case", deny_unknown_fields)]
141pub enum RemoteObjectPayloads {
142 SpooledInline,
145 SpooledExternal,
150 RowBlob { locator_bytes: Vec<u8> },
153}
154
155impl RemoteObjectPayloads {
156 pub fn carried_locator_bytes(&self) -> Option<&[u8]> {
159 match self {
160 Self::RowBlob { locator_bytes } => Some(locator_bytes),
161 Self::SpooledInline | Self::SpooledExternal => None,
162 }
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct ClosedRemoteObject {
176 record: RemoteObjectRecord,
177 payloads: BTreeMap<ObjectHash, Vec<u8>>,
178}
179
180impl ClosedRemoteObject {
181 pub(crate) fn carried(record: RemoteObjectRecord) -> Result<Self, RemoteObjectRecordError> {
185 Self::with_payloads(record, BTreeMap::new())
186 }
187
188 fn with_spooled_payloads(
192 record: RemoteObjectRecord,
193 canonical_semantic_bytes: &[u8],
194 stored_bytes: &[u8],
195 ) -> Result<Self, RemoteObjectRecordError> {
196 let mut payloads = BTreeMap::new();
197 if let SemanticPayload::Spooled(hash) = record.semantic_payload() {
198 payloads.insert(hash, canonical_semantic_bytes.to_vec());
199 }
200 if let Some(hash) = record.stored_payload() {
201 record.object().verify(stored_bytes)?;
202 payloads.insert(hash, stored_bytes.to_vec());
203 }
204 Self::with_payloads(record, payloads)
205 }
206
207 pub fn with_payloads(
214 record: RemoteObjectRecord,
215 payloads: BTreeMap<ObjectHash, Vec<u8>>,
216 ) -> Result<Self, RemoteObjectRecordError> {
217 if payloads.keys().copied().collect::<BTreeSet<_>>() != record.payload_claims() {
218 return Err(RemoteObjectRecordError::PayloadPlacement);
219 }
220 Ok(Self { record, payloads })
221 }
222
223 pub fn record(&self) -> &RemoteObjectRecord {
224 &self.record
225 }
226
227 pub fn into_record(self) -> RemoteObjectRecord {
228 self.record
229 }
230
231 pub fn map_record(
236 self,
237 transition: impl FnOnce(
238 RemoteObjectRecord,
239 ) -> Result<RemoteObjectRecord, RemoteObjectRecordError>,
240 ) -> Result<Self, RemoteObjectRecordError> {
241 Self::with_payloads(transition(self.record)?, self.payloads)
242 }
243
244 pub fn payload_bytes(&self) -> &BTreeMap<ObjectHash, Vec<u8>> {
248 &self.payloads
249 }
250
251 pub fn semantic_bytes(&self) -> Option<&[u8]> {
255 match self.record.semantic_payload() {
256 SemanticPayload::Carried(bytes) => Some(bytes),
257 SemanticPayload::Spooled(hash) => self.payloads.get(&hash).map(Vec::as_slice),
258 SemanticPayload::Absent => None,
259 }
260 }
261
262 pub fn stored_bytes(&self) -> Option<&[u8]> {
264 self.record
265 .stored_payload()
266 .and_then(|hash| self.payloads.get(&hash).map(Vec::as_slice))
267 }
268}
269
270impl std::ops::Deref for ClosedRemoteObject {
271 type Target = RemoteObjectRecord;
272
273 fn deref(&self) -> &Self::Target {
274 &self.record
275 }
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub enum SemanticPayload<'record> {
283 Carried(&'record [u8]),
284 Spooled(ObjectHash),
285 Absent,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case", deny_unknown_fields)]
290pub enum CandidateObjectState {
291 Prepared {
292 ownership: PendingCandidateOwnership,
293 },
294 UploadedVerified {
295 ownership: PendingCandidateOwnership,
296 },
297 CleanupPending {
298 former_candidates: Vec<CandidateNonactivation>,
299 },
300 AbsentVerified {
301 former_candidates: Vec<CandidateNonactivation>,
302 },
303}
304
305impl CandidateObjectState {
306 fn validate(&self) -> Result<(), RemoteObjectRecordError> {
307 match self {
308 Self::Prepared { ownership } | Self::UploadedVerified { ownership } => {
309 ownership.validate()
310 }
311 Self::CleanupPending { former_candidates }
312 | Self::AbsentVerified { former_candidates } => {
313 validate_nonactivations(former_candidates)
314 }
315 }
316 }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
320#[serde(rename_all = "snake_case", deny_unknown_fields)]
321pub enum CandidateCommitState {
322 Prepared,
323 UploadedVerified,
324 CleanupPending { proof: CandidateNonactivationProof },
325 AbsentVerified { proof: CandidateNonactivationProof },
326}
327
328pub use super::store_commit::StoreBatchCommitDeletionTarget;
329
330#[derive(Debug, thiserror::Error)]
331pub enum RemoteObjectRecordError {
332 #[error("prepared stored bytes do not match their exact reference: {0}")]
333 Storage(#[from] crate::objects::StorageError),
334 #[error("remote object JSON: {0}")]
335 Json(#[from] serde_json::Error),
336 #[error("remote object Store protocol: {0}")]
337 StoreProtocol(#[from] crate::store_commit::StoreProtocolError),
338 #[error("remote object audience package: {0}")]
339 AudiencePackage(#[from] crate::audience_package::AudiencePackageError),
340 #[error("remote object Circle transition: {0}")]
341 CircleTransition(#[from] crate::circle_control::CircleTransitionError),
342 #[error("remote object blob locator: {0}")]
343 BlobLocator(#[from] crate::blob::locator::BlobLocatorError),
344 #[error("remote object payload placement contradicts its domain")]
345 PayloadPlacement,
346 #[error("prepared stored reference differs from the closed identity reference")]
347 StoredReferenceMismatch,
348 #[error("prepared semantic hash is {actual}, expected {expected}")]
349 SemanticHashMismatch {
350 expected: ObjectHash,
351 actual: ObjectHash,
352 },
353 #[error("pending candidate ownership has no pending candidate")]
354 EmptyPendingOwnership,
355 #[error("candidate ownership sets overlap")]
356 OverlappingOwnership,
357 #[error("candidate ownership has no pending or activated owner")]
358 EmptyOwnership,
359 #[error("prepared canonical bytes do not parse as their claimed domain: {0}")]
360 InvalidDomain(String),
361 #[error("prepared canonical bytes disagree with their claimed domain")]
362 DomainMismatch,
363 #[error("candidate object graph contains the same exact object more than once")]
364 DuplicateCandidateObject,
365 #[error("candidate object graph material is missing")]
366 CandidateObjectMissing,
367 #[error("candidate object material is outside the signed graph")]
368 CandidateObjectInvented,
369 #[error("remote object is not uploaded for the exact activating commit")]
370 InvalidActivation,
371 #[error("remote object cannot return to uploaded state after cleanup began")]
372 InvalidUploadTransition,
373 #[error("candidate nonactivation set is empty")]
374 EmptyNonactivation,
375 #[error("candidate nonactivation proof is invalid: {0}")]
376 InvalidProof(String),
377 #[error("candidate nonactivation proof has invalid Store protocol: {0}")]
378 InvalidProofProtocol(#[source] crate::store_commit::StoreProtocolError),
379 #[error("candidate does not own this remote object")]
380 CandidateOwnerMismatch,
381 #[error("remote object does not retain this candidate's nonactivation proof")]
382 CandidateNonactivationMissing,
383 #[error("remote object is not awaiting exact candidate cleanup")]
384 InvalidCleanupTransition,
385 #[error("remote object is not the solely-owned activated Store package being reclaimed")]
386 InvalidReclaim,
387}
388
389#[cfg(test)]
390mod tests;