1use super::device_state::merge_history_cuts;
2use super::*;
3
4pub use coven_foundation::object_hash::ObjectHash;
5
6#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct StoreCommitCoord {
10 pub stream_id: AuthorStreamId,
11 pub sequence: u64,
12}
13
14impl StoreCommitCoord {
15 pub fn sequence(&self) -> u64 {
16 self.sequence
17 }
18
19 pub fn validate(&self) -> Result<(), StoreProtocolError> {
20 if self.sequence() == 0 {
21 return Err(StoreProtocolError::Malformed(
22 "Store commit coordinate uses sequence zero".to_string(),
23 ));
24 }
25 Ok(())
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
31#[serde(transparent)]
32pub struct CandidateFamilyId(ObjectHash);
33
34impl CandidateFamilyId {
35 pub fn from_hash(hash: ObjectHash) -> Self {
36 Self(hash)
37 }
38
39 pub fn as_hash(self) -> ObjectHash {
40 self.0
41 }
42
43 pub fn derive(
44 store_root_hash: ObjectHash,
45 author_registration: &StoreDeviceRegistrationRef,
46 write_id: &WriteId,
47 order: &StoreCommitOrder,
48 ) -> Self {
49 #[derive(Serialize)]
50 struct Fields<'a> {
51 store_root_hash: ObjectHash,
52 author_registration: &'a StoreDeviceRegistrationRef,
53 write_id: &'a WriteId,
54 sequence: u64,
55 predecessor: Option<&'a StoreBatchCommitRef>,
56 }
57 let fields = Fields {
58 store_root_hash,
59 author_registration,
60 write_id,
61 sequence: order.seq(),
62 predecessor: order.predecessor.as_ref(),
63 };
64 Self(ObjectHash::digest(&domain_json(
65 CANDIDATE_FAMILY_DOMAIN,
66 &fields,
67 )))
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct StoreBatchCommitRef {
75 pub coord: StoreCommitCoord,
76 pub commit_hash: ObjectHash,
77 pub object: ExactObjectRef,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct StoreBatchCommitDeletionTarget {
84 pub coord: StoreCommitCoord,
85 pub object: ExactObjectRef,
86 pub canonical_signed_bytes: Vec<u8>,
87}
88
89impl StoreBatchCommitDeletionTarget {
90 pub(crate) fn verify_candidate(
91 &self,
92 expected_store_root_hash: ObjectHash,
93 author: &StoreDeviceRegistration,
94 ) -> Result<VerifiedStoreBatchCommit, StoreProtocolError> {
95 let commit = self.verify_exact_candidate(expected_store_root_hash, author)?;
96 if matches!(&commit.body, StoreCommitBody::AbandonCandidates { .. }) {
97 return Err(StoreProtocolError::Malformed(
98 "retained authority cannot be a candidate cleanup target".to_string(),
99 ));
100 }
101 Ok(commit)
102 }
103
104 pub fn verify_nonactivation_candidate(
105 &self,
106 expected_store_root_hash: ObjectHash,
107 author: &StoreDeviceRegistration,
108 ) -> Result<VerifiedStoreBatchCommit, StoreProtocolError> {
109 self.verify_exact_candidate(expected_store_root_hash, author)
110 }
111
112 fn verify_exact_candidate(
113 &self,
114 expected_store_root_hash: ObjectHash,
115 author: &StoreDeviceRegistration,
116 ) -> Result<VerifiedStoreBatchCommit, StoreProtocolError> {
117 self.object.verify(&self.canonical_signed_bytes)?;
118 let commit = VerifiedStoreBatchCommit::parse_prepared(
119 &self.canonical_signed_bytes,
120 expected_store_root_hash,
121 self.coord.clone(),
122 self.object.clone(),
123 author,
124 )?;
125 if commit.to_bytes() != self.canonical_signed_bytes {
126 return Err(StoreProtocolError::Malformed(
127 "candidate commit bytes are not canonical".to_string(),
128 ));
129 }
130 Ok(commit)
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
135#[serde(deny_unknown_fields)]
136pub struct CandidateCleanupManifest {
137 pub candidate: StoreBatchCommitDeletionTarget,
138}
139
140impl StoreBatchCommitRef {
141 pub fn from_commit(
142 commit: &StoreBatchCommit,
143 coord: StoreCommitCoord,
144 object: ExactObjectRef,
145 ) -> Result<Self, StoreProtocolError> {
146 if coord.sequence() != commit.seq() {
147 return Err(StoreProtocolError::Malformed(
148 "Store commit reference coordinate differs from the signed commit".to_string(),
149 ));
150 }
151 let reference = Self {
152 coord,
153 commit_hash: commit.commit_hash(),
154 object,
155 };
156 reference.verify_commit(commit)?;
157 Ok(reference)
158 }
159
160 pub fn verify_commit(&self, commit: &StoreBatchCommit) -> Result<(), StoreProtocolError> {
161 if self.coord.sequence() != commit.seq() || self.commit_hash != commit.commit_hash() {
162 return Err(StoreProtocolError::Malformed(
163 "exact Store commit reference differs from the signed commit".to_string(),
164 ));
165 }
166 let stream_id = commit_stream_id(&self.coord);
167 let expected = format!(
168 "{}.json",
169 commit_semantic_prefix(
170 commit.candidate_family(),
171 &stream_id,
172 self.coord.sequence(),
173 self.commit_hash,
174 )
175 );
176 if self.object.slot().logical_key() != expected {
177 return Err(StoreProtocolError::RelocatedSlot {
178 expected,
179 actual: self.object.slot().logical_key().to_string(),
180 });
181 }
182 Ok(())
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
187#[serde(deny_unknown_fields)]
188pub struct StoreRootRef {
189 pub store_root_id: ObjectHash,
190 pub store_root_hash: ObjectHash,
191 pub object: ExactObjectRef,
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
195#[serde(transparent)]
196pub struct StreamActivationId(ObjectHash);
197
198impl StreamActivationId {
199 pub fn from_digest(hash: ObjectHash) -> Self {
200 Self(hash)
201 }
202
203 pub fn as_hash(self) -> ObjectHash {
204 self.0
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
209#[serde(rename_all = "snake_case", deny_unknown_fields)]
210pub enum StreamActivation {
211 GrantAuthorized {
212 store_root_hash: ObjectHash,
213 author_registration: StoreDeviceRegistrationRef,
214 grant_id: MembershipGrantId,
215 anchor: GrantStreamAnchor,
216 },
217 DeviceAuthorized {
218 store_root_hash: ObjectHash,
219 author_registration: StoreDeviceRegistrationRef,
220 anchor: DeviceStreamAnchor,
221 },
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct RegisteredStreamActivation {
226 activation: StreamActivation,
227 activating_commit: StoreBatchCommitRef,
228}
229
230impl RegisteredStreamActivation {
231 pub fn from_stored(
232 stored_activation_id: StreamActivationId,
233 stored_author_stream_id: AuthorStreamId,
234 activation: StreamActivation,
235 activating_commit: StoreBatchCommitRef,
236 ) -> Result<Self, StoreProtocolError> {
237 if activation.activation_id() != stored_activation_id {
238 return Err(StoreProtocolError::Malformed(
239 "stored stream activation id differs from its canonical descriptor".to_string(),
240 ));
241 }
242 if activation.author_stream_id() != stored_author_stream_id {
243 return Err(StoreProtocolError::Malformed(
244 "stored author stream id differs from its canonical descriptor".to_string(),
245 ));
246 }
247 Ok(Self {
248 activation,
249 activating_commit,
250 })
251 }
252
253 pub fn activation(&self) -> &StreamActivation {
254 &self.activation
255 }
256
257 pub fn activating_commit(&self) -> &StoreBatchCommitRef {
258 &self.activating_commit
259 }
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
263#[serde(rename_all = "snake_case")]
264pub enum StreamAnchorDomain {
265 StoreMembership,
266 OwnerRecovery,
267 CircleControl { circle_id: CircleId },
268 CircleRoster { circle_id: CircleId },
269 CircleMetadata { circle_id: CircleId },
270 CircleAcknowledgements { circle_id: CircleId },
271 CircleSnapshots { circle_id: CircleId },
272 StoreAnnouncements,
273 StoreAcknowledgements,
274 StoreSnapshots,
275}
276
277impl GrantStreamAnchor {
278 fn domain(&self) -> StreamAnchorDomain {
279 match self {
280 Self::StoreMembership { .. } => StreamAnchorDomain::StoreMembership,
281 Self::OwnerRecovery { .. } => StreamAnchorDomain::OwnerRecovery,
282 Self::CircleControl { circle_id, .. } => StreamAnchorDomain::CircleControl {
283 circle_id: *circle_id,
284 },
285 Self::CircleRoster { circle_id, .. } => StreamAnchorDomain::CircleRoster {
286 circle_id: *circle_id,
287 },
288 Self::CircleMetadata { circle_id, .. } => StreamAnchorDomain::CircleMetadata {
289 circle_id: *circle_id,
290 },
291 }
292 }
293}
294
295impl DeviceStreamAnchor {
296 fn domain(&self) -> StreamAnchorDomain {
297 match self {
298 Self::StoreAnnouncements { .. } => StreamAnchorDomain::StoreAnnouncements,
299 Self::StoreAcknowledgements { .. } => StreamAnchorDomain::StoreAcknowledgements,
300 Self::StoreSnapshots { .. } => StreamAnchorDomain::StoreSnapshots,
301 Self::CircleAcknowledgements { circle_id, .. } => {
302 StreamAnchorDomain::CircleAcknowledgements {
303 circle_id: *circle_id,
304 }
305 }
306 Self::CircleSnapshots { circle_id, .. } => StreamAnchorDomain::CircleSnapshots {
307 circle_id: *circle_id,
308 },
309 }
310 }
311}
312
313impl StreamActivation {
314 pub fn grant_authorized(
315 store_root_hash: ObjectHash,
316 author_registration: StoreDeviceRegistrationRef,
317 grant_id: MembershipGrantId,
318 anchor: GrantStreamAnchor,
319 ) -> Self {
320 Self::GrantAuthorized {
321 store_root_hash,
322 author_registration,
323 grant_id,
324 anchor,
325 }
326 }
327
328 pub fn device_authorized(
329 store_root_hash: ObjectHash,
330 author_registration: StoreDeviceRegistrationRef,
331 anchor: DeviceStreamAnchor,
332 ) -> Self {
333 Self::DeviceAuthorized {
334 store_root_hash,
335 author_registration,
336 anchor,
337 }
338 }
339
340 pub fn activation_id(&self) -> StreamActivationId {
341 StreamActivationId(ObjectHash::digest(&domain_json(
342 STREAM_ACTIVATION_ID_DOMAIN,
343 self,
344 )))
345 }
346
347 pub fn author_stream_id(&self) -> AuthorStreamId {
348 match self {
349 Self::GrantAuthorized {
350 store_root_hash,
351 author_registration,
352 grant_id,
353 anchor,
354 } => derive_grant_author_stream_id(
355 *store_root_hash,
356 author_registration,
357 grant_id,
358 anchor.domain(),
359 ),
360 Self::DeviceAuthorized {
361 store_root_hash,
362 author_registration,
363 anchor,
364 } => derive_device_author_stream_id(
365 *store_root_hash,
366 author_registration,
367 anchor.domain(),
368 ),
369 }
370 }
371
372 pub fn device_authorized_stream_id(
373 store_root_hash: ObjectHash,
374 author_registration: &StoreDeviceRegistrationRef,
375 domain: StreamAnchorDomain,
376 ) -> AuthorStreamId {
377 derive_device_author_stream_id(store_root_hash, author_registration, domain)
378 }
379
380 pub fn grant_authorized_stream_id(
381 store_root_hash: ObjectHash,
382 author_registration: &StoreDeviceRegistrationRef,
383 grant_id: &MembershipGrantId,
384 domain: StreamAnchorDomain,
385 ) -> AuthorStreamId {
386 derive_grant_author_stream_id(store_root_hash, author_registration, grant_id, domain)
387 }
388
389 pub fn first_slot(&self) -> &ObjectSlot {
390 match self {
391 Self::GrantAuthorized { anchor, .. } => anchor.first_slot(),
392 Self::DeviceAuthorized { anchor, .. } => anchor.first_slot(),
393 }
394 }
395
396 pub fn author_registration(&self) -> &StoreDeviceRegistrationRef {
397 match self {
398 Self::GrantAuthorized {
399 author_registration,
400 ..
401 }
402 | Self::DeviceAuthorized {
403 author_registration,
404 ..
405 } => author_registration,
406 }
407 }
408
409 pub fn store_root_hash(&self) -> ObjectHash {
410 match self {
411 Self::GrantAuthorized {
412 store_root_hash, ..
413 }
414 | Self::DeviceAuthorized {
415 store_root_hash, ..
416 } => *store_root_hash,
417 }
418 }
419}
420
421#[derive(Serialize)]
422struct GrantAuthorStreamFields<'a> {
423 store_root_hash: ObjectHash,
424 domain: StreamAnchorDomain,
425 author_registration: &'a StoreDeviceRegistrationRef,
426 grant_id: &'a MembershipGrantId,
427}
428
429#[derive(Serialize)]
430struct DeviceAuthorStreamFields<'a> {
431 store_root_hash: ObjectHash,
432 domain: StreamAnchorDomain,
433 author_registration: &'a StoreDeviceRegistrationRef,
434}
435
436fn derive_grant_author_stream_id(
437 store_root_hash: ObjectHash,
438 author_registration: &StoreDeviceRegistrationRef,
439 grant_id: &MembershipGrantId,
440 domain: StreamAnchorDomain,
441) -> AuthorStreamId {
442 derive_author_stream_id(&GrantAuthorStreamFields {
443 store_root_hash,
444 domain,
445 author_registration,
446 grant_id,
447 })
448}
449
450fn derive_device_author_stream_id(
451 store_root_hash: ObjectHash,
452 author_registration: &StoreDeviceRegistrationRef,
453 domain: StreamAnchorDomain,
454) -> AuthorStreamId {
455 derive_author_stream_id(&DeviceAuthorStreamFields {
456 store_root_hash,
457 domain,
458 author_registration,
459 })
460}
461
462fn derive_author_stream_id(fields: &impl Serialize) -> AuthorStreamId {
463 AuthorStreamId::from_digest(ObjectHash::digest(&domain_json(
464 AUTHOR_STREAM_ID_DOMAIN,
465 fields,
466 )))
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
470#[serde(deny_unknown_fields)]
471pub struct SuccessorLink {
472 pub activation: StreamActivationId,
473 pub predecessor: Option<ExactObjectRef>,
474 pub next_slot: ObjectSlot,
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
479#[serde(transparent)]
480pub struct CommitFrontier(pub BTreeMap<AuthorStreamId, StoreBatchCommitRef>);
481
482#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
484#[serde(transparent)]
485pub struct StoreHistoryCut(pub BTreeMap<AuthorStreamId, StoreBatchCommitRef>);
486
487impl StoreHistoryCut {
488 pub fn from_commits(commits: BTreeMap<AuthorStreamId, StoreBatchCommitRef>) -> Self {
489 Self(commits)
490 }
491
492 pub fn position_count(&self) -> usize {
493 self.0.len()
494 }
495
496 pub fn commits(&self) -> &BTreeMap<AuthorStreamId, StoreBatchCommitRef> {
497 &self.0
498 }
499
500 pub fn frontier(&self) -> CommitFrontier {
501 CommitFrontier(self.0.clone())
502 }
503
504 pub fn join(self, other: Self) -> Result<Self, StoreProtocolError> {
505 merge_history_cuts(self, other)
506 }
507}
508
509impl CommitFrontier {
510 pub fn from_refs(
511 commits: BTreeMap<String, StoreBatchCommitRef>,
512 ) -> Result<Self, StoreProtocolError> {
513 commits
514 .into_iter()
515 .map(|(stream_id, commit)| {
516 let stream_id = stream_id
517 .parse()
518 .map_err(StoreProtocolError::AuthorStreamId)?;
519 Ok((stream_id, commit))
520 })
521 .collect::<Result<BTreeMap<_, _>, _>>()
522 .map(Self)
523 }
524
525 pub fn into_refs(self) -> BTreeMap<String, StoreBatchCommitRef> {
526 self.0
527 .into_iter()
528 .map(|(stream_id, commit)| (stream_id.to_string(), commit))
529 .collect()
530 }
531
532 pub fn position_count(&self) -> usize {
533 self.0.len()
534 }
535
536 pub fn covers(&self, covered: &Self) -> bool {
537 covered
538 .0
539 .iter()
540 .all(|(stream, covered_ref)| self.covers_commit_on_stream(stream, covered_ref))
541 }
542
543 pub fn commits(&self) -> &BTreeMap<AuthorStreamId, StoreBatchCommitRef> {
544 &self.0
545 }
546
547 pub fn covers_commit(&self, commit: &StoreBatchCommitRef) -> bool {
548 self.covers_commit_on_stream(&commit.coord.stream_id, commit)
549 }
550
551 fn covers_commit_on_stream(
552 &self,
553 stream: &AuthorStreamId,
554 covered: &StoreBatchCommitRef,
555 ) -> bool {
556 self.0.get(stream).is_some_and(|current| {
557 current.coord.sequence() > covered.coord.sequence()
558 || current.coord.sequence() == covered.coord.sequence() && current == covered
559 })
560 }
561
562 pub fn join(self, other: Self) -> Result<Self, StoreProtocolError> {
563 StoreHistoryCut::from_commits(self.0)
564 .join(StoreHistoryCut::from_commits(other.0))
565 .map(|cut| cut.frontier())
566 }
567}
568
569#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
571#[serde(deny_unknown_fields)]
572pub struct StoreCommitOrder {
573 pub seq: u64,
574 pub predecessor: Option<StoreBatchCommitRef>,
575 pub dependencies: BTreeMap<AuthorStreamId, StoreBatchCommitRef>,
576}
577
578impl StoreCommitOrder {
579 pub fn seq(&self) -> u64 {
580 self.seq
581 }
582
583 pub fn predecessor(&self) -> Option<&StoreBatchCommitRef> {
584 self.predecessor.as_ref()
585 }
586
587 pub fn dependencies(&self) -> &BTreeMap<AuthorStreamId, StoreBatchCommitRef> {
588 &self.dependencies
589 }
590
591 pub fn stream_id<'a>(&self, device_id: &'a str) -> &'a str {
592 device_id
593 }
594
595 pub fn predecessor_cut(&self) -> Result<StoreHistoryCut, StoreProtocolError> {
596 let mut cut = self.dependencies.clone();
597 if let Some(predecessor) = &self.predecessor {
598 if cut
599 .insert(predecessor.coord.stream_id, predecessor.clone())
600 .is_some_and(|existing| existing != *predecessor)
601 {
602 return Err(StoreProtocolError::JoinAttemptMismatch);
603 }
604 }
605 Ok(StoreHistoryCut(cut))
606 }
607}
608
609pub(super) fn commit_stream_id(coord: &StoreCommitCoord) -> String {
610 coord.stream_id.to_string()
611}