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