1use std::collections::BTreeMap;
4use std::collections::BTreeSet;
5use std::fmt;
6use std::str::FromStr;
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use sha2::{Digest, Sha256};
10
11use super::membership::{
12 verify_membership_entry, AuthorStreamId, MembershipChange, MembershipCoord, MembershipEntry,
13 MembershipGrantCreationAuthority, MembershipGrantId,
14};
15use super::storage::{ExactObjectRef, ProviderDeviceBinding};
16use crate::keys::{self, UserKeypair};
17use crate::storage::cloud::ObjectSlot;
18use crate::sync::circle::{
19 AccessLeafId, CircleControlCoord, CircleEpochId, CircleId, CircleMetadataCoord,
20 CircleMetadataHeadRef, CircleRosterConflictResolutionRef, CircleRosterCoord,
21 CircleRosterHeadRef,
22};
23use crate::sync::circle_control::StoreMembershipStateRef;
24use crate::KeyFingerprint;
25use crate::{WriteId, WritePolicy};
26
27mod ordered_map_entries {
28 use std::collections::BTreeMap;
29
30 use serde::{Deserialize, Deserializer, Serialize, Serializer};
31
32 pub(super) fn serialize<K, V, S>(map: &BTreeMap<K, V>, serializer: S) -> Result<S::Ok, S::Error>
33 where
34 K: Ord + Serialize,
35 V: Serialize,
36 S: Serializer,
37 {
38 map.iter().collect::<Vec<_>>().serialize(serializer)
39 }
40
41 pub(super) fn deserialize<'de, K, V, D>(deserializer: D) -> Result<BTreeMap<K, V>, D::Error>
42 where
43 K: Ord + Deserialize<'de>,
44 V: Deserialize<'de>,
45 D: Deserializer<'de>,
46 {
47 let entries = Vec::<(K, V)>::deserialize(deserializer)?;
48 let entry_count = entries.len();
49 let map = entries.into_iter().collect::<BTreeMap<_, _>>();
50 if map.len() != entry_count {
51 return Err(serde::de::Error::custom(
52 "ordered map entries contain a duplicate key",
53 ));
54 }
55 Ok(map)
56 }
57}
58
59pub const STORE_PROTOCOL_VERSION: u32 = 1;
60
61pub(crate) const STORE_PROTOCOL_PREFIX: &str = "store-v1/";
62pub const STORE_PROTOCOL_ROOT_SEMANTIC_PATH: &str = "store-v1/store-protocol-root";
63pub const STORE_PROTOCOL_ROOT_LOGICAL_KEY: &str = "store-v1/store-protocol-root.json";
64pub(crate) const STORE_CANDIDATE_PREFIX: &str = "store-v1/candidates/";
65pub(crate) const STORE_HEAD_PREFIX: &str = "store-v1/heads/";
66pub(crate) const STORE_ACK_PREFIX: &str = "store-v1/acks/";
67pub(crate) const STORE_DEVICE_REGISTRATION_PREFIX: &str = "store-v1/devices/";
68pub(crate) const STORE_DEVICE_JOIN_ATTEMPT_PREFIX: &str = "store-v1/device-join-attempts/";
69pub(crate) const STORE_DEVICE_JOIN_OUTCOME_PREFIX: &str = "store-v1/device-join-outcomes/";
70pub(crate) const STORE_DEVICE_JOIN_CLEANUP_RECEIPT_PREFIX: &str =
71 "store-v1/device-join-cleanup-receipts/";
72pub(crate) const STORE_PROVIDER_ACCESS_GRANT_PREFIX: &str = "store-v1/provider-access/grants/";
73pub(crate) const STORE_PROVIDER_ACCESS_WITHDRAWAL_PREFIX: &str =
74 "store-v1/provider-access/withdrawals/";
75pub(crate) const STORE_OWNER_RECOVERY_PREFIX: &str = "store-v1/recovery/";
76pub(crate) const STORE_SNAPSHOT_META_PREFIX: &str = "store-v1/snapshots/";
77pub(crate) const STORE_SNAPSHOT_IMAGE_PREFIX: &str = "store-v1/snapshot-images/";
78pub(crate) const STORE_MEMBERSHIP_ENTRY_PREFIX: &str = "store-v1/membership/entries/";
79pub(crate) const STORE_MEMBERSHIP_HEAD_PREFIX: &str = "store-v1/membership/heads/";
80const STORE_SERIAL_HEAD_KEY: &str = "store-v1/heads/serial.json";
81
82const STORE_PROTOCOL_ROOT_DOMAIN: &[u8] = b"coven.store-protocol-root.v1\0";
83const COMMIT_DOMAIN: &[u8] = b"coven.store-batch-commit.v1\0";
84const HEAD_DOMAIN: &[u8] = b"coven.store-device-head.v1\0";
85const SERIAL_HEAD_DOMAIN: &[u8] = b"coven.store-serial-head.v1\0";
86const REGISTRATION_DOMAIN: &[u8] = b"coven.store-device-registration.v1\0";
87const SELF_RETIREMENT_DOMAIN: &[u8] = b"coven.store-device-self-retirement.v1\0";
88const DEVICE_JOIN_ATTEMPT_DOMAIN: &[u8] = b"coven.device-join-attempt.v1\0";
89const DEVICE_READINESS_DOMAIN: &[u8] = b"coven.device-readiness.v1\0";
90const DEVICE_JOIN_OUTCOME_DOMAIN: &[u8] = b"coven.device-join-outcome.v1\0";
91const OWNER_RECOVERY_NODE_DOMAIN: &[u8] = b"coven.owner-recovery-node.v1\0";
92const ACK_DOMAIN: &[u8] = b"coven.store-ack.v1\0";
93const SNAPSHOT_DOMAIN: &[u8] = b"coven.snapshot-meta.v1\0";
94const CANDIDATE_FAMILY_DOMAIN: &[u8] = b"coven.candidate-family.v1\0";
95
96#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
97pub struct ObjectHash([u8; 32]);
98
99impl ObjectHash {
100 pub fn digest(bytes: &[u8]) -> Self {
101 Self(Sha256::digest(bytes).into())
102 }
103
104 pub(crate) fn from_digest(bytes: [u8; 32]) -> Self {
105 Self(bytes)
106 }
107
108 pub fn as_bytes(&self) -> &[u8; 32] {
109 &self.0
110 }
111}
112
113impl fmt::Debug for ObjectHash {
114 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115 fmt::Display::fmt(self, formatter)
116 }
117}
118
119impl fmt::Display for ObjectHash {
120 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121 formatter.write_str(&hex::encode(self.0))
122 }
123}
124
125impl FromStr for ObjectHash {
126 type Err = StoreProtocolError;
127
128 fn from_str(value: &str) -> Result<Self, Self::Err> {
129 if value.len() != 64
130 || value
131 .bytes()
132 .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
133 {
134 return Err(StoreProtocolError::InvalidObjectHash(value.to_string()));
135 }
136 let decoded = hex::decode(value)
137 .map_err(|_| StoreProtocolError::InvalidObjectHash(value.to_string()))?;
138 let bytes: [u8; 32] = decoded
139 .try_into()
140 .map_err(|_| StoreProtocolError::InvalidObjectHash(value.to_string()))?;
141 Ok(Self(bytes))
142 }
143}
144
145impl Serialize for ObjectHash {
146 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
147 where
148 S: Serializer,
149 {
150 serializer.serialize_str(&self.to_string())
151 }
152}
153
154impl<'de> Deserialize<'de> for ObjectHash {
155 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
156 where
157 D: Deserializer<'de>,
158 {
159 String::deserialize(deserializer)?
160 .parse()
161 .map_err(serde::de::Error::custom)
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
167#[serde(rename_all = "snake_case", deny_unknown_fields)]
168pub enum StoreCommitCoord {
169 MergeConcurrent {
170 stream_id: AuthorStreamId,
171 sequence: u64,
172 },
173 Serial {
174 sequence: u64,
175 },
176}
177
178impl StoreCommitCoord {
179 pub fn sequence(&self) -> u64 {
180 match self {
181 Self::MergeConcurrent { sequence, .. } | Self::Serial { sequence } => *sequence,
182 }
183 }
184
185 pub fn policy(&self) -> WritePolicy {
186 match self {
187 Self::MergeConcurrent { .. } => WritePolicy::MergeConcurrent,
188 Self::Serial { .. } => WritePolicy::Serial,
189 }
190 }
191
192 pub fn validate(&self) -> Result<(), StoreProtocolError> {
193 if self.sequence() == 0 {
194 return Err(StoreProtocolError::Malformed(
195 "Store commit coordinate uses sequence zero".to_string(),
196 ));
197 }
198 Ok(())
199 }
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
204#[serde(transparent)]
205pub struct CandidateFamilyId(ObjectHash);
206
207impl CandidateFamilyId {
208 pub fn from_hash(hash: ObjectHash) -> Self {
209 Self(hash)
210 }
211
212 pub fn as_hash(self) -> ObjectHash {
213 self.0
214 }
215
216 pub fn derive(
217 store_root_hash: ObjectHash,
218 author_registration: &StoreDeviceRegistrationRef,
219 write_id: &WriteId,
220 order: &StoreCommitOrder,
221 ) -> Self {
222 #[derive(Serialize)]
223 struct Fields<'a> {
224 store_root_hash: ObjectHash,
225 author_registration: &'a StoreDeviceRegistrationRef,
226 write_id: &'a WriteId,
227 policy: WritePolicy,
228 sequence: u64,
229 predecessor: CandidateFamilyPredecessor<'a>,
230 }
231
232 #[derive(Serialize)]
233 #[serde(rename_all = "snake_case")]
234 enum CandidateFamilyPredecessor<'a> {
235 Merge(Option<&'a StoreBatchCommitRef>),
236 SerialGenesis {
237 root: &'a StoreRootRef,
238 founder_registration: &'a StoreDeviceRegistrationRef,
239 },
240 SerialCommit(&'a StoreBatchCommitRef),
241 }
242
243 let predecessor = match order {
244 StoreCommitOrder::MergeConcurrent { predecessor, .. } => {
245 CandidateFamilyPredecessor::Merge(predecessor.as_ref())
246 }
247 StoreCommitOrder::Serial {
248 predecessor:
249 StoreSerialPredecessor::Genesis {
250 root,
251 founder_registration,
252 },
253 ..
254 } => CandidateFamilyPredecessor::SerialGenesis {
255 root,
256 founder_registration,
257 },
258 StoreCommitOrder::Serial {
259 predecessor: StoreSerialPredecessor::Commit(predecessor),
260 ..
261 } => CandidateFamilyPredecessor::SerialCommit(predecessor),
262 };
263 let fields = Fields {
264 store_root_hash,
265 author_registration,
266 write_id,
267 policy: order.policy(),
268 sequence: order.seq(),
269 predecessor,
270 };
271 Self(ObjectHash::digest(&domain_json(
272 CANDIDATE_FAMILY_DOMAIN,
273 &fields,
274 )))
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
280#[serde(deny_unknown_fields)]
281pub struct StoreBatchCommitRef {
282 pub coord: StoreCommitCoord,
283 pub commit_hash: ObjectHash,
284 pub object: ExactObjectRef,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
289#[serde(deny_unknown_fields)]
290pub struct StoreBatchCommitDeletionTarget {
291 pub coord: StoreCommitCoord,
292 pub object: ExactObjectRef,
293 pub canonical_signed_bytes: Vec<u8>,
294}
295
296impl StoreBatchCommitDeletionTarget {
297 pub(crate) fn verify_candidate(
298 &self,
299 expected_store_root_hash: ObjectHash,
300 author: &StoreDeviceRegistration,
301 ) -> Result<StoreBatchCommit, StoreProtocolError> {
302 self.object
303 .verify(&self.canonical_signed_bytes)
304 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
305 let commit: StoreBatchCommit = serde_json::from_slice(&self.canonical_signed_bytes)
306 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
307 if commit.to_bytes() != self.canonical_signed_bytes {
308 return Err(StoreProtocolError::Malformed(
309 "candidate commit bytes are not canonical".to_string(),
310 ));
311 }
312 if matches!(
313 &commit.body,
314 StoreCommitBody::SerialRecoveryActivation { .. }
315 | StoreCommitBody::AbandonCandidates { .. }
316 ) {
317 return Err(StoreProtocolError::Malformed(
318 "retained authority cannot be a candidate cleanup target".to_string(),
319 ));
320 }
321 commit.verify_at(expected_store_root_hash, &self.coord, author)?;
322 StoreBatchCommitRef::from_commit(&commit, self.coord.clone(), self.object.clone())?;
323 Ok(commit)
324 }
325}
326
327#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
328#[serde(deny_unknown_fields)]
329pub struct CandidateCleanupManifest {
330 pub candidate: StoreBatchCommitDeletionTarget,
331}
332
333impl StoreBatchCommitRef {
334 pub fn from_commit(
335 commit: &StoreBatchCommit,
336 coord: StoreCommitCoord,
337 object: ExactObjectRef,
338 ) -> Result<Self, StoreProtocolError> {
339 if coord.policy() != commit.policy() || coord.sequence() != commit.seq() {
340 return Err(StoreProtocolError::Malformed(
341 "Store commit reference coordinate differs from the signed commit".to_string(),
342 ));
343 }
344 let reference = Self {
345 coord,
346 commit_hash: commit.commit_hash(),
347 object,
348 };
349 reference.verify_commit(commit)?;
350 Ok(reference)
351 }
352
353 pub fn verify_commit(&self, commit: &StoreBatchCommit) -> Result<(), StoreProtocolError> {
354 if self.coord.policy() != commit.policy()
355 || self.coord.sequence() != commit.seq()
356 || self.commit_hash != commit.commit_hash()
357 {
358 return Err(StoreProtocolError::Malformed(
359 "exact Store commit reference differs from the signed commit".to_string(),
360 ));
361 }
362 let stream_id = commit_stream_id(&self.coord);
363 let expected = format!(
364 "{}.json",
365 commit_semantic_prefix(
366 commit.candidate_family(),
367 &stream_id,
368 self.coord.sequence(),
369 self.commit_hash,
370 )
371 );
372 if self.object.slot().logical_key() != expected {
373 return Err(StoreProtocolError::RelocatedSlot {
374 expected,
375 actual: self.object.slot().logical_key().to_string(),
376 });
377 }
378 Ok(())
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
383#[serde(deny_unknown_fields)]
384pub struct StoreRootRef {
385 pub store_root_id: ObjectHash,
386 pub store_root_hash: ObjectHash,
387 pub object: ExactObjectRef,
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
391#[serde(transparent)]
392pub struct StreamActivationId(ObjectHash);
393
394impl StreamActivationId {
395 pub fn from_hash(hash: ObjectHash) -> Self {
396 Self(hash)
397 }
398
399 pub fn store_announcements(
400 root: &StoreRootRef,
401 registration: &StoreDeviceRegistrationRef,
402 ) -> Self {
403 Self(ObjectHash::digest(
404 &serde_json::to_vec(&(root, registration))
405 .expect("Store announcement activation serialization cannot fail"),
406 ))
407 }
408
409 pub fn store_acknowledgements(
410 root: &StoreRootRef,
411 registration: &StoreDeviceRegistrationRef,
412 ) -> Self {
413 Self(ObjectHash::digest(
414 &serde_json::to_vec(&(root, registration, "acknowledgements"))
415 .expect("Store acknowledgement activation serialization cannot fail"),
416 ))
417 }
418
419 pub fn store_snapshots(root: &StoreRootRef, registration: &StoreDeviceRegistrationRef) -> Self {
420 Self(ObjectHash::digest(
421 &serde_json::to_vec(&(root, registration, "snapshots"))
422 .expect("Store snapshot activation serialization cannot fail"),
423 ))
424 }
425
426 pub fn store_membership(
427 root: &StoreRootRef,
428 registration: &StoreDeviceRegistrationRef,
429 grant: &MembershipGrantId,
430 anchor: &GrantStreamAnchor,
431 ) -> Self {
432 Self(ObjectHash::digest(
433 &serde_json::to_vec(&(root, registration, grant, anchor))
434 .expect("Store membership activation serialization cannot fail"),
435 ))
436 }
437
438 pub(crate) fn circle_stream<T: Serialize>(
439 root: &StoreRootRef,
440 circle_id: CircleId,
441 domain: &'static str,
442 stream: &T,
443 ) -> Self {
444 Self(ObjectHash::digest(
445 &serde_json::to_vec(&(root, circle_id, domain, stream))
446 .expect("Circle stream activation serialization cannot fail"),
447 ))
448 }
449
450 pub fn as_hash(self) -> ObjectHash {
451 self.0
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
456#[serde(deny_unknown_fields)]
457pub struct SuccessorLink {
458 pub activation: StreamActivationId,
459 pub predecessor: Option<ExactObjectRef>,
460 pub next_slot: ObjectSlot,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465#[serde(rename_all = "snake_case", deny_unknown_fields)]
466pub enum CommitFrontier {
467 MergeConcurrent(BTreeMap<AuthorStreamId, StoreBatchCommitRef>),
468 Serial(Option<StoreBatchCommitRef>),
469}
470
471#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
473#[serde(rename_all = "snake_case", deny_unknown_fields)]
474pub enum StoreHistoryCut {
475 MergeConcurrent(BTreeMap<AuthorStreamId, StoreBatchCommitRef>),
476 Serial(StoreSerialPredecessor),
477}
478
479impl StoreHistoryCut {
480 pub fn merge_concurrent(commits: BTreeMap<AuthorStreamId, StoreBatchCommitRef>) -> Self {
481 Self::MergeConcurrent(commits)
482 }
483
484 pub fn serial(predecessor: StoreSerialPredecessor) -> Self {
485 Self::Serial(predecessor)
486 }
487
488 pub fn policy(&self) -> WritePolicy {
489 match self {
490 Self::MergeConcurrent(_) => WritePolicy::MergeConcurrent,
491 Self::Serial(_) => WritePolicy::Serial,
492 }
493 }
494
495 pub fn position_count(&self) -> usize {
496 match self {
497 Self::MergeConcurrent(commits) => commits.len(),
498 Self::Serial(_) => 1,
499 }
500 }
501
502 pub fn serial_predecessor(&self) -> Option<&StoreSerialPredecessor> {
503 match self {
504 Self::Serial(predecessor) => Some(predecessor),
505 Self::MergeConcurrent(_) => None,
506 }
507 }
508}
509
510impl CommitFrontier {
511 pub fn from_refs(
512 policy: WritePolicy,
513 mut commits: BTreeMap<String, StoreBatchCommitRef>,
514 ) -> Result<Self, StoreProtocolError> {
515 match policy {
516 WritePolicy::MergeConcurrent => commits
517 .into_iter()
518 .map(|(stream_id, commit)| {
519 let stream_id = stream_id.parse().map_err(StoreProtocolError::Malformed)?;
520 Ok((stream_id, commit))
521 })
522 .collect::<Result<BTreeMap<_, _>, _>>()
523 .map(Self::MergeConcurrent),
524 WritePolicy::Serial => {
525 let commit = commits.remove(SERIAL_STREAM_ID);
526 if !commits.is_empty() {
527 return Err(StoreProtocolError::Malformed(format!(
528 "Serial frontier contains non-serial streams: {:?}",
529 commits.keys().collect::<Vec<_>>()
530 )));
531 }
532 if commit.as_ref().is_some_and(|reference| {
533 !matches!(reference.coord, StoreCommitCoord::Serial { .. })
534 }) {
535 return Err(StoreProtocolError::Malformed(
536 "Serial frontier contains a Merge commit".to_string(),
537 ));
538 }
539 Ok(Self::Serial(commit))
540 }
541 }
542 }
543
544 pub fn into_refs(self) -> BTreeMap<String, StoreBatchCommitRef> {
545 match self {
546 Self::MergeConcurrent(commits) => commits
547 .into_iter()
548 .map(|(stream_id, commit)| (stream_id.to_string(), commit))
549 .collect(),
550 Self::Serial(Some(commit)) => BTreeMap::from([(SERIAL_STREAM_ID.to_string(), commit)]),
551 Self::Serial(None) => BTreeMap::new(),
552 }
553 }
554
555 pub fn position_count(&self) -> usize {
556 match self {
557 Self::MergeConcurrent(positions) => positions.len(),
558 Self::Serial(Some(_)) => 1,
559 Self::Serial(None) => 0,
560 }
561 }
562
563 pub fn policy(&self) -> WritePolicy {
564 match self {
565 Self::MergeConcurrent(_) => WritePolicy::MergeConcurrent,
566 Self::Serial(_) => WritePolicy::Serial,
567 }
568 }
569
570 pub fn merge_commits(
571 &self,
572 ) -> Result<&BTreeMap<AuthorStreamId, StoreBatchCommitRef>, StoreProtocolError> {
573 match self {
574 Self::MergeConcurrent(commits) => Ok(commits),
575 Self::Serial(_) => Err(StoreProtocolError::WritePolicyMismatch {
576 expected: WritePolicy::MergeConcurrent,
577 actual: WritePolicy::Serial,
578 }),
579 }
580 }
581
582 pub fn serial_commit(&self) -> Result<Option<&StoreBatchCommitRef>, StoreProtocolError> {
583 match self {
584 Self::Serial(position) => Ok(position.as_ref()),
585 Self::MergeConcurrent(_) => Err(StoreProtocolError::WritePolicyMismatch {
586 expected: WritePolicy::Serial,
587 actual: WritePolicy::MergeConcurrent,
588 }),
589 }
590 }
591}
592
593#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
595#[serde(rename_all = "snake_case", deny_unknown_fields)]
596pub enum StoreCommitOrder {
597 MergeConcurrent {
598 seq: u64,
599 predecessor: Option<StoreBatchCommitRef>,
600 dependencies: BTreeMap<AuthorStreamId, StoreBatchCommitRef>,
601 },
602 Serial {
603 seq: u64,
604 predecessor: SerialStorePosition,
605 },
606}
607
608#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
609#[serde(rename_all = "snake_case", deny_unknown_fields)]
610pub enum SerialStorePosition {
611 Genesis {
612 root: StoreRootRef,
613 founder_registration: StoreDeviceRegistrationRef,
614 },
615 Commit(StoreBatchCommitRef),
616}
617
618pub type StoreSerialPredecessor = SerialStorePosition;
619
620impl StoreCommitOrder {
621 pub fn policy(&self) -> WritePolicy {
622 match self {
623 Self::MergeConcurrent { .. } => WritePolicy::MergeConcurrent,
624 Self::Serial { .. } => WritePolicy::Serial,
625 }
626 }
627
628 pub fn seq(&self) -> u64 {
629 match self {
630 Self::MergeConcurrent { seq, .. } | Self::Serial { seq, .. } => *seq,
631 }
632 }
633
634 pub fn predecessor(&self) -> Option<&StoreBatchCommitRef> {
635 match self {
636 Self::MergeConcurrent { predecessor, .. } => predecessor.as_ref(),
637 Self::Serial {
638 predecessor: StoreSerialPredecessor::Commit(predecessor),
639 ..
640 } => Some(predecessor),
641 Self::Serial {
642 predecessor: StoreSerialPredecessor::Genesis { .. },
643 ..
644 } => None,
645 }
646 }
647
648 pub fn dependencies(&self) -> Option<&BTreeMap<AuthorStreamId, StoreBatchCommitRef>> {
649 match self {
650 Self::MergeConcurrent { dependencies, .. } => Some(dependencies),
651 Self::Serial { .. } => None,
652 }
653 }
654
655 pub fn stream_id<'a>(&self, device_id: &'a str) -> &'a str {
656 match self {
657 Self::MergeConcurrent { .. } => device_id,
658 Self::Serial { .. } => SERIAL_STREAM_ID,
659 }
660 }
661
662 pub fn predecessor_cut(&self) -> Result<StoreHistoryCut, StoreProtocolError> {
663 match self {
664 Self::MergeConcurrent {
665 predecessor,
666 dependencies,
667 ..
668 } => {
669 let mut cut = dependencies.clone();
670 if let Some(predecessor) = predecessor {
671 let StoreCommitCoord::MergeConcurrent { stream_id, .. } = predecessor.coord
672 else {
673 return Err(StoreProtocolError::JoinAttemptMismatch);
674 };
675 if cut
676 .insert(stream_id, predecessor.clone())
677 .is_some_and(|existing| existing != *predecessor)
678 {
679 return Err(StoreProtocolError::JoinAttemptMismatch);
680 }
681 }
682 Ok(StoreHistoryCut::MergeConcurrent(cut))
683 }
684 Self::Serial { predecessor, .. } => Ok(StoreHistoryCut::Serial(predecessor.clone())),
685 }
686 }
687}
688
689pub const SERIAL_STREAM_ID: &str = "serial";
690
691fn commit_stream_id(coord: &StoreCommitCoord) -> String {
692 match coord {
693 StoreCommitCoord::MergeConcurrent { stream_id, .. } => stream_id.to_string(),
694 StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
695 }
696}
697
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(deny_unknown_fields)]
700pub struct StorePackageRef {
701 pub candidate_family: CandidateFamilyId,
702 pub content_hash: ObjectHash,
703 pub schema_version: u32,
704 pub changeset_size: u64,
705 pub object: ExactObjectRef,
706}
707
708#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
709#[serde(deny_unknown_fields)]
710pub struct CirclePackageRef {
711 pub circle_id: CircleId,
712 pub control: CircleControlCoord,
713 pub package: StorePackageRef,
714 pub key_fingerprint: KeyFingerprint,
715}
716
717#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
719#[serde(deny_unknown_fields)]
720pub struct CircleHeadObjectRef {
721 pub object: ExactObjectRef,
722 pub successor: SuccessorLink,
723}
724
725#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
727#[serde(deny_unknown_fields)]
728pub struct CircleAccessEnvelopeObjectRef {
729 pub owner_pubkey: String,
730 pub recipient_slot: String,
731 pub control_hash: ObjectHash,
732 pub leaf_id: AccessLeafId,
733 pub leaf_hash: ObjectHash,
734 pub object: ExactObjectRef,
735}
736
737#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
739#[serde(deny_unknown_fields)]
740pub struct CircleAccessLeafObjectRef {
741 pub owner_pubkey: String,
742 pub epoch_id: CircleEpochId,
743 pub recipient_slot: String,
744 pub leaf_id: AccessLeafId,
745 pub leaf_hash: ObjectHash,
746 pub object: ExactObjectRef,
747}
748
749#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
750#[serde(deny_unknown_fields)]
751pub struct CircleAccessObjectRef {
752 pub leaf: CircleAccessLeafObjectRef,
753 pub envelope: CircleAccessEnvelopeObjectRef,
754}
755
756#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
758#[serde(deny_unknown_fields)]
759pub struct CircleMetadataObjectRef {
760 pub key_fingerprint: KeyFingerprint,
761 pub object: ExactObjectRef,
762}
763
764#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
766#[serde(deny_unknown_fields)]
767pub struct CircleActivationObjects {
768 pub control: ExactObjectRef,
769 pub control_head: Option<CircleHeadObjectRef>,
770 #[serde(with = "ordered_map_entries")]
771 pub roster_entries: BTreeMap<CircleRosterCoord, ExactObjectRef>,
772 #[serde(with = "ordered_map_entries")]
773 pub roster_heads: BTreeMap<CircleRosterHeadRef, CircleHeadObjectRef>,
774 #[serde(with = "ordered_map_entries")]
775 pub roster_resolutions: BTreeMap<CircleRosterConflictResolutionRef, ExactObjectRef>,
776 #[serde(with = "ordered_map_entries")]
777 pub metadata_entries: BTreeMap<CircleMetadataCoord, CircleMetadataObjectRef>,
778 #[serde(with = "ordered_map_entries")]
779 pub metadata_heads: BTreeMap<CircleMetadataHeadRef, CircleHeadObjectRef>,
780 pub access: Vec<CircleAccessObjectRef>,
781}
782
783#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
784#[serde(rename_all = "snake_case", deny_unknown_fields)]
785pub enum CircleControlRef {
786 MergeConcurrent {
787 circle_id: CircleId,
788 control: CircleControlCoord,
789 head_hash: ObjectHash,
790 objects: CircleActivationObjects,
791 },
792 Serial {
793 circle_id: CircleId,
794 control: CircleControlCoord,
795 objects: CircleActivationObjects,
796 },
797}
798
799impl CircleControlRef {
800 pub fn circle_id(&self) -> CircleId {
801 match self {
802 Self::MergeConcurrent { circle_id, .. } | Self::Serial { circle_id, .. } => *circle_id,
803 }
804 }
805
806 pub fn control(&self) -> &CircleControlCoord {
807 match self {
808 Self::MergeConcurrent { control, .. } | Self::Serial { control, .. } => control,
809 }
810 }
811
812 pub fn head_hash(&self) -> Option<ObjectHash> {
813 match self {
814 Self::MergeConcurrent { head_hash, .. } => Some(*head_hash),
815 Self::Serial { .. } => None,
816 }
817 }
818
819 pub fn objects(&self) -> &CircleActivationObjects {
820 match self {
821 Self::MergeConcurrent { objects, .. } | Self::Serial { objects, .. } => objects,
822 }
823 }
824}
825
826#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
827#[serde(deny_unknown_fields)]
828pub struct StoreDeviceRegistrationRef {
829 pub device_id: StoreDeviceId,
830 pub registration_hash: ObjectHash,
831 pub object: ExactObjectRef,
832}
833
834impl StoreDeviceRegistrationRef {
835 pub fn from_registration(
836 registration: &StoreDeviceRegistration,
837 object: ExactObjectRef,
838 ) -> Self {
839 Self {
840 device_id: registration.device_id,
841 registration_hash: registration.registration_hash(),
842 object,
843 }
844 }
845
846 pub fn verify_registration(
847 &self,
848 registration: &StoreDeviceRegistration,
849 ) -> Result<(), StoreProtocolError> {
850 if registration.device_id != self.device_id
851 || registration.registration_hash() != self.registration_hash
852 {
853 return Err(StoreProtocolError::DeviceRegistrationRefMismatch {
854 device_id: self.device_id.to_string(),
855 revision: 1,
856 expected: self.registration_hash,
857 actual: registration.registration_hash(),
858 });
859 }
860 Ok(())
861 }
862}
863
864pub struct CirclePackageInput<'a> {
865 pub circle_id: CircleId,
866 pub control: CircleControlCoord,
867 pub key_fingerprint: KeyFingerprint,
868 pub package: StorePackageInput<'a>,
869}
870
871pub struct StorePackageInput<'a> {
872 pub candidate_family: CandidateFamilyId,
873 pub schema_version: u32,
874 pub bytes: &'a [u8],
875 pub object: ExactObjectRef,
876}
877
878#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
879#[serde(rename_all = "snake_case", deny_unknown_fields)]
880pub enum StoreControl {
881 SerialMembership {
882 entry: super::membership::SerialMembershipEntry,
883 },
884 SerialMembershipAndKeyRotation {
885 entry: super::membership::SerialMembershipEntry,
886 generation: u64,
887 },
888 ProviderAdmin {
889 change: super::provider::ProviderAdminChange,
890 },
891}
892
893#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
896#[serde(deny_unknown_fields)]
897pub struct SerialRecoveryActivation {
898 pub registration: ActivatedStoreDeviceRegistrationRef,
899}
900
901impl StoreControl {
902 pub fn serial_membership_entry(&self) -> Option<&super::membership::SerialMembershipEntry> {
903 match self {
904 Self::SerialMembership { entry }
905 | Self::SerialMembershipAndKeyRotation { entry, .. } => Some(entry),
906 Self::ProviderAdmin { .. } => None,
907 }
908 }
909
910 pub fn key_generation(&self) -> Option<u64> {
911 match self {
912 Self::SerialMembership { .. } => None,
913 Self::SerialMembershipAndKeyRotation { generation, .. } => Some(*generation),
914 Self::ProviderAdmin { .. } => None,
915 }
916 }
917}
918
919#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
920#[serde(deny_unknown_fields)]
921pub struct CandidateObjectManifest {
922 pub family: CandidateFamilyId,
923 pub objects: Vec<CandidateExclusiveObjectRef>,
924}
925
926#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
927#[serde(rename_all = "snake_case", deny_unknown_fields)]
928pub enum CandidateExclusiveObjectRef {
929 StorePackage(StorePackageRef),
930 CirclePackage(CirclePackageRef),
931 CircleAccess {
932 circle_id: CircleId,
933 access: CircleAccessObjectRef,
934 },
935 SelfRetirement(StoreDeviceSelfRetirementRef),
936}
937
938#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
939#[serde(deny_unknown_fields)]
940pub struct StoreCommitOperations {
941 pub control: Option<StoreControl>,
942 pub device_join_attempts: Vec<DeviceJoinAttemptRef>,
943 pub device_join_outcomes: Vec<DeviceJoinOutcomeRef>,
944 pub device_join_abandonments: Vec<super::device_join::DeviceJoinAbandonmentRef>,
945 pub device_join_cleanup_receipts: Vec<super::device_join::DeviceJoinCleanupReceiptRef>,
946 pub provider_access_grants: Vec<super::provider::StoreMemberProviderAccessGrantRef>,
947 pub provider_access_withdrawals:
948 Vec<super::provider::StoreMemberProviderAccessWithdrawalReceiptRef>,
949 pub device_registrations: Vec<ActivatedStoreDeviceRegistrationRef>,
950 pub circle_controls: Vec<CircleControlRef>,
951 pub store_package: Option<StorePackageRef>,
952 pub circle_packages: Vec<CirclePackageRef>,
953}
954
955impl StoreCommitOperations {
956 fn is_empty(&self) -> bool {
957 self.control.is_none()
958 && self.device_join_attempts.is_empty()
959 && self.device_join_outcomes.is_empty()
960 && self.device_join_abandonments.is_empty()
961 && self.device_join_cleanup_receipts.is_empty()
962 && self.provider_access_grants.is_empty()
963 && self.provider_access_withdrawals.is_empty()
964 && self.device_registrations.is_empty()
965 && self.circle_controls.is_empty()
966 && self.store_package.is_none()
967 && self.circle_packages.is_empty()
968 }
969}
970
971#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
972#[serde(rename_all = "snake_case", deny_unknown_fields)]
973pub enum StoreCommitBody {
974 Operations(StoreCommitOperations),
975 SelfRetirement {
976 retirement: StoreDeviceSelfRetirementRef,
977 },
978 SerialRecoveryActivation {
979 activation: SerialRecoveryActivation,
980 },
981 AbandonCandidates {
982 manifests: Vec<CandidateCleanupManifest>,
983 },
984}
985
986pub struct StoreCommitOperationsInput<'a> {
987 pub control: Option<StoreControl>,
988 pub device_join_attempts: Vec<DeviceJoinAttemptRef>,
989 pub device_join_outcomes: Vec<DeviceJoinOutcomeRef>,
990 pub device_join_abandonments: Vec<super::device_join::DeviceJoinAbandonmentRef>,
991 pub device_join_cleanup_receipts: Vec<super::device_join::DeviceJoinCleanupReceiptRef>,
992 pub provider_access_grants: Vec<super::provider::StoreMemberProviderAccessGrantRef>,
993 pub provider_access_withdrawals:
994 Vec<super::provider::StoreMemberProviderAccessWithdrawalReceiptRef>,
995 pub device_registrations: Vec<ActivatedStoreDeviceRegistrationRef>,
996 pub circle_controls: Vec<CircleControlRef>,
997 pub store_package: Option<StorePackageInput<'a>>,
998 pub circle_packages: &'a [CirclePackageInput<'a>],
999}
1000
1001#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1002#[serde(deny_unknown_fields)]
1003pub struct StoreBatchCommit {
1004 pub version: u32,
1005 pub store_root_hash: ObjectHash,
1006 pub write_id: WriteId,
1007 pub author_registration: StoreDeviceRegistrationRef,
1008 pub order: StoreCommitOrder,
1009 pub membership_state: StoreMembershipStateRef,
1010 pub device_state: StoreDeviceStateRef,
1011 pub membership_authority: Option<MembershipGrantCreationAuthority>,
1012 pub candidate_objects: CandidateObjectManifest,
1013 pub body: StoreCommitBody,
1014 pub signature: String,
1015}
1016
1017#[derive(Serialize)]
1018struct CommitSignedFields<'a> {
1019 version: u32,
1020 store_root_hash: ObjectHash,
1021 write_id: &'a WriteId,
1022 author_registration: &'a StoreDeviceRegistrationRef,
1023 order: &'a StoreCommitOrder,
1024 membership_state: &'a StoreMembershipStateRef,
1025 device_state: &'a StoreDeviceStateRef,
1026 membership_authority: Option<&'a MembershipGrantCreationAuthority>,
1027 candidate_objects: &'a CandidateObjectManifest,
1028 body: &'a StoreCommitBody,
1029}
1030
1031impl StoreBatchCommit {
1032 pub fn policy(&self) -> WritePolicy {
1033 self.order.policy()
1034 }
1035
1036 pub fn seq(&self) -> u64 {
1037 self.order.seq()
1038 }
1039
1040 pub fn candidate_family(&self) -> CandidateFamilyId {
1041 CandidateFamilyId::derive(
1042 self.store_root_hash,
1043 &self.author_registration,
1044 &self.write_id,
1045 &self.order,
1046 )
1047 }
1048
1049 pub fn operations(&self) -> Option<&StoreCommitOperations> {
1050 match &self.body {
1051 StoreCommitBody::Operations(operations) => Some(operations),
1052 StoreCommitBody::SelfRetirement { .. }
1053 | StoreCommitBody::SerialRecoveryActivation { .. }
1054 | StoreCommitBody::AbandonCandidates { .. } => None,
1055 }
1056 }
1057
1058 pub fn control(&self) -> Option<&StoreControl> {
1059 self.operations()
1060 .and_then(|operations| operations.control.as_ref())
1061 }
1062
1063 pub fn serial_recovery_activation(&self) -> Option<&SerialRecoveryActivation> {
1064 match &self.body {
1065 StoreCommitBody::SerialRecoveryActivation { activation } => Some(activation),
1066 StoreCommitBody::Operations(_)
1067 | StoreCommitBody::SelfRetirement { .. }
1068 | StoreCommitBody::AbandonCandidates { .. } => None,
1069 }
1070 }
1071
1072 pub fn self_retirement(&self) -> Option<&StoreDeviceSelfRetirementRef> {
1073 match &self.body {
1074 StoreCommitBody::SelfRetirement { retirement } => Some(retirement),
1075 StoreCommitBody::Operations(_)
1076 | StoreCommitBody::SerialRecoveryActivation { .. }
1077 | StoreCommitBody::AbandonCandidates { .. } => None,
1078 }
1079 }
1080
1081 pub fn abandoned_candidates(&self) -> &[CandidateCleanupManifest] {
1082 match &self.body {
1083 StoreCommitBody::AbandonCandidates { manifests } => manifests,
1084 StoreCommitBody::Operations(_)
1085 | StoreCommitBody::SelfRetirement { .. }
1086 | StoreCommitBody::SerialRecoveryActivation { .. } => &[],
1087 }
1088 }
1089
1090 pub fn device_join_attempts(&self) -> &[DeviceJoinAttemptRef] {
1091 self.operations()
1092 .map_or(&[], |operations| operations.device_join_attempts.as_slice())
1093 }
1094
1095 pub fn device_join_outcomes(&self) -> &[DeviceJoinOutcomeRef] {
1096 self.operations()
1097 .map_or(&[], |operations| operations.device_join_outcomes.as_slice())
1098 }
1099
1100 pub fn device_join_abandonments(&self) -> &[super::device_join::DeviceJoinAbandonmentRef] {
1101 self.operations().map_or(&[], |operations| {
1102 operations.device_join_abandonments.as_slice()
1103 })
1104 }
1105
1106 pub fn device_join_cleanup_receipts(
1107 &self,
1108 ) -> &[super::device_join::DeviceJoinCleanupReceiptRef] {
1109 self.operations().map_or(&[], |operations| {
1110 operations.device_join_cleanup_receipts.as_slice()
1111 })
1112 }
1113
1114 pub fn provider_access_grants(&self) -> &[super::provider::StoreMemberProviderAccessGrantRef] {
1115 self.operations().map_or(&[], |operations| {
1116 operations.provider_access_grants.as_slice()
1117 })
1118 }
1119
1120 pub fn provider_access_withdrawals(
1121 &self,
1122 ) -> &[super::provider::StoreMemberProviderAccessWithdrawalReceiptRef] {
1123 self.operations().map_or(&[], |operations| {
1124 operations.provider_access_withdrawals.as_slice()
1125 })
1126 }
1127
1128 pub fn device_registrations(&self) -> &[ActivatedStoreDeviceRegistrationRef] {
1129 match &self.body {
1130 StoreCommitBody::Operations(operations) => operations.device_registrations.as_slice(),
1131 StoreCommitBody::SerialRecoveryActivation { activation } => {
1132 std::slice::from_ref(&activation.registration)
1133 }
1134 StoreCommitBody::SelfRetirement { .. } => &[],
1135 StoreCommitBody::AbandonCandidates { .. } => &[],
1136 }
1137 }
1138
1139 pub fn device_retirements(&self) -> &[StoreDeviceSelfRetirementRef] {
1140 match &self.body {
1141 StoreCommitBody::SelfRetirement { retirement } => std::slice::from_ref(retirement),
1142 StoreCommitBody::Operations(_) | StoreCommitBody::SerialRecoveryActivation { .. } => {
1143 &[]
1144 }
1145 StoreCommitBody::AbandonCandidates { .. } => &[],
1146 }
1147 }
1148
1149 pub fn circle_controls(&self) -> &[CircleControlRef] {
1150 self.operations()
1151 .map_or(&[], |operations| operations.circle_controls.as_slice())
1152 }
1153
1154 pub fn store_package(&self) -> Option<&StorePackageRef> {
1155 self.operations()
1156 .and_then(|operations| operations.store_package.as_ref())
1157 }
1158
1159 pub fn circle_packages(&self) -> &[CirclePackageRef] {
1160 self.operations()
1161 .map_or(&[], |operations| operations.circle_packages.as_slice())
1162 }
1163
1164 pub fn merge_dependencies(
1165 &self,
1166 ) -> Result<&BTreeMap<AuthorStreamId, StoreBatchCommitRef>, StoreProtocolError> {
1167 match &self.order {
1168 StoreCommitOrder::MergeConcurrent { dependencies, .. } => Ok(dependencies),
1169 StoreCommitOrder::Serial { .. } => Err(StoreProtocolError::WritePolicyMismatch {
1170 expected: WritePolicy::MergeConcurrent,
1171 actual: WritePolicy::Serial,
1172 }),
1173 }
1174 }
1175
1176 #[allow(clippy::too_many_arguments)]
1177 pub fn signed(
1178 store_root_hash: ObjectHash,
1179 write_id: WriteId,
1180 coord: StoreCommitCoord,
1181 author_registration: StoreDeviceRegistrationRef,
1182 author: &StoreDeviceRegistration,
1183 order: StoreCommitOrder,
1184 membership_state: StoreMembershipStateRef,
1185 device_state: StoreDeviceStateRef,
1186 membership_authority: Option<MembershipGrantCreationAuthority>,
1187 package: StorePackageInput<'_>,
1188 signer: &UserKeypair,
1189 ) -> Result<Self, StoreProtocolError> {
1190 Self::signed_operations(
1191 store_root_hash,
1192 write_id,
1193 coord,
1194 author_registration,
1195 author,
1196 order,
1197 membership_state,
1198 device_state,
1199 membership_authority,
1200 StoreCommitOperationsInput {
1201 control: None,
1202 device_join_attempts: Vec::new(),
1203 device_join_outcomes: Vec::new(),
1204 device_join_abandonments: Vec::new(),
1205 device_join_cleanup_receipts: Vec::new(),
1206 provider_access_grants: Vec::new(),
1207 provider_access_withdrawals: Vec::new(),
1208 device_registrations: Vec::new(),
1209 circle_controls: Vec::new(),
1210 store_package: Some(package),
1211 circle_packages: &[],
1212 },
1213 signer,
1214 )
1215 }
1216
1217 #[allow(clippy::too_many_arguments)]
1218 pub fn signed_with_control(
1219 store_root_hash: ObjectHash,
1220 write_id: WriteId,
1221 coord: StoreCommitCoord,
1222 author_registration: StoreDeviceRegistrationRef,
1223 author: &StoreDeviceRegistration,
1224 order: StoreCommitOrder,
1225 membership_state: StoreMembershipStateRef,
1226 device_state: StoreDeviceStateRef,
1227 membership_authority: Option<MembershipGrantCreationAuthority>,
1228 control: Option<StoreControl>,
1229 package: Option<StorePackageInput<'_>>,
1230 signer: &UserKeypair,
1231 ) -> Result<Self, StoreProtocolError> {
1232 Self::signed_operations(
1233 store_root_hash,
1234 write_id,
1235 coord,
1236 author_registration,
1237 author,
1238 order,
1239 membership_state,
1240 device_state,
1241 membership_authority,
1242 StoreCommitOperationsInput {
1243 control,
1244 device_join_attempts: Vec::new(),
1245 device_join_outcomes: Vec::new(),
1246 device_join_abandonments: Vec::new(),
1247 device_join_cleanup_receipts: Vec::new(),
1248 provider_access_grants: Vec::new(),
1249 provider_access_withdrawals: Vec::new(),
1250 device_registrations: Vec::new(),
1251 circle_controls: Vec::new(),
1252 store_package: package,
1253 circle_packages: &[],
1254 },
1255 signer,
1256 )
1257 }
1258
1259 #[allow(clippy::too_many_arguments)]
1260 pub fn signed_with_registrations(
1261 store_root_hash: ObjectHash,
1262 write_id: WriteId,
1263 coord: StoreCommitCoord,
1264 author_registration: StoreDeviceRegistrationRef,
1265 author: &StoreDeviceRegistration,
1266 order: StoreCommitOrder,
1267 membership_state: StoreMembershipStateRef,
1268 device_state: StoreDeviceStateRef,
1269 membership_authority: Option<MembershipGrantCreationAuthority>,
1270 device_registrations: Vec<ActivatedStoreDeviceRegistrationRef>,
1271 signer: &UserKeypair,
1272 ) -> Result<Self, StoreProtocolError> {
1273 Self::signed_operations(
1274 store_root_hash,
1275 write_id,
1276 coord,
1277 author_registration,
1278 author,
1279 order,
1280 membership_state,
1281 device_state,
1282 membership_authority,
1283 StoreCommitOperationsInput {
1284 control: None,
1285 device_join_attempts: Vec::new(),
1286 device_join_outcomes: Vec::new(),
1287 device_join_abandonments: Vec::new(),
1288 device_join_cleanup_receipts: Vec::new(),
1289 provider_access_grants: Vec::new(),
1290 provider_access_withdrawals: Vec::new(),
1291 device_registrations,
1292 circle_controls: Vec::new(),
1293 store_package: None,
1294 circle_packages: &[],
1295 },
1296 signer,
1297 )
1298 }
1299
1300 #[allow(clippy::too_many_arguments)]
1301 pub fn signed_with_serial_recovery(
1302 store_root_hash: ObjectHash,
1303 write_id: WriteId,
1304 coord: StoreCommitCoord,
1305 author_registration: StoreDeviceRegistrationRef,
1306 author: &StoreDeviceRegistration,
1307 order: StoreCommitOrder,
1308 membership_state: StoreMembershipStateRef,
1309 device_state: StoreDeviceStateRef,
1310 activation: SerialRecoveryActivation,
1311 signer: &UserKeypair,
1312 ) -> Result<Self, StoreProtocolError> {
1313 validate_commit_envelope(
1314 store_root_hash,
1315 &coord,
1316 &author_registration,
1317 author,
1318 &order,
1319 &membership_state,
1320 &device_state,
1321 None,
1322 signer,
1323 )?;
1324 validate_serial_recovery_activation(&order, &activation, &author_registration)?;
1325 Self::finish_signed_body(
1326 store_root_hash,
1327 write_id,
1328 author_registration,
1329 order,
1330 membership_state,
1331 device_state,
1332 None,
1333 StoreCommitBody::SerialRecoveryActivation { activation },
1334 signer,
1335 )
1336 }
1337
1338 #[allow(clippy::too_many_arguments)]
1339 pub fn signed_with_self_retirement(
1340 store_root_hash: ObjectHash,
1341 write_id: WriteId,
1342 coord: StoreCommitCoord,
1343 author_registration: StoreDeviceRegistrationRef,
1344 author: &StoreDeviceRegistration,
1345 order: StoreCommitOrder,
1346 membership_state: StoreMembershipStateRef,
1347 device_state: StoreDeviceStateRef,
1348 membership_authority: Option<MembershipGrantCreationAuthority>,
1349 retirement: StoreDeviceSelfRetirementRef,
1350 signer: &UserKeypair,
1351 ) -> Result<Self, StoreProtocolError> {
1352 validate_commit_envelope(
1353 store_root_hash,
1354 &coord,
1355 &author_registration,
1356 author,
1357 &order,
1358 &membership_state,
1359 &device_state,
1360 membership_authority.as_ref(),
1361 signer,
1362 )?;
1363 validate_device_retirement_refs(
1364 std::slice::from_ref(&retirement),
1365 CandidateFamilyId::derive(store_root_hash, &author_registration, &write_id, &order),
1366 &author_registration,
1367 &order,
1368 )?;
1369 Self::finish_signed_body(
1370 store_root_hash,
1371 write_id,
1372 author_registration,
1373 order,
1374 membership_state,
1375 device_state,
1376 membership_authority,
1377 StoreCommitBody::SelfRetirement { retirement },
1378 signer,
1379 )
1380 }
1381
1382 #[allow(clippy::too_many_arguments)]
1383 pub fn signed_with_candidate_abandonment(
1384 store_root_hash: ObjectHash,
1385 write_id: WriteId,
1386 coord: StoreCommitCoord,
1387 author_registration: StoreDeviceRegistrationRef,
1388 author: &StoreDeviceRegistration,
1389 order: StoreCommitOrder,
1390 membership_state: StoreMembershipStateRef,
1391 device_state: StoreDeviceStateRef,
1392 mut manifests: Vec<CandidateCleanupManifest>,
1393 signer: &UserKeypair,
1394 ) -> Result<Self, StoreProtocolError> {
1395 validate_commit_envelope(
1396 store_root_hash,
1397 &coord,
1398 &author_registration,
1399 author,
1400 &order,
1401 &membership_state,
1402 &device_state,
1403 None,
1404 signer,
1405 )?;
1406 manifests.sort();
1407 validate_candidate_abandonment(
1408 &manifests,
1409 store_root_hash,
1410 &author_registration,
1411 &coord,
1412 &order,
1413 author,
1414 )?;
1415 Self::finish_signed_body(
1416 store_root_hash,
1417 write_id,
1418 author_registration,
1419 order,
1420 membership_state,
1421 device_state,
1422 None,
1423 StoreCommitBody::AbandonCandidates { manifests },
1424 signer,
1425 )
1426 }
1427
1428 #[allow(clippy::too_many_arguments)]
1429 pub fn signed_with_join_attempts(
1430 store_root_hash: ObjectHash,
1431 write_id: WriteId,
1432 coord: StoreCommitCoord,
1433 author_registration: StoreDeviceRegistrationRef,
1434 author: &StoreDeviceRegistration,
1435 order: StoreCommitOrder,
1436 membership_state: StoreMembershipStateRef,
1437 device_state: StoreDeviceStateRef,
1438 membership_authority: Option<MembershipGrantCreationAuthority>,
1439 device_join_attempts: Vec<DeviceJoinAttemptRef>,
1440 signer: &UserKeypair,
1441 ) -> Result<Self, StoreProtocolError> {
1442 Self::signed_operations(
1443 store_root_hash,
1444 write_id,
1445 coord,
1446 author_registration,
1447 author,
1448 order,
1449 membership_state,
1450 device_state,
1451 membership_authority,
1452 StoreCommitOperationsInput {
1453 control: None,
1454 device_join_attempts,
1455 device_join_outcomes: Vec::new(),
1456 device_join_abandonments: Vec::new(),
1457 device_join_cleanup_receipts: Vec::new(),
1458 provider_access_grants: Vec::new(),
1459 provider_access_withdrawals: Vec::new(),
1460 device_registrations: Vec::new(),
1461 circle_controls: Vec::new(),
1462 store_package: None,
1463 circle_packages: &[],
1464 },
1465 signer,
1466 )
1467 }
1468
1469 #[allow(clippy::too_many_arguments)]
1470 pub fn signed_with_join_outcomes(
1471 store_root_hash: ObjectHash,
1472 write_id: WriteId,
1473 coord: StoreCommitCoord,
1474 author_registration: StoreDeviceRegistrationRef,
1475 author: &StoreDeviceRegistration,
1476 order: StoreCommitOrder,
1477 membership_state: StoreMembershipStateRef,
1478 device_state: StoreDeviceStateRef,
1479 membership_authority: Option<MembershipGrantCreationAuthority>,
1480 device_join_outcomes: Vec<DeviceJoinOutcomeRef>,
1481 device_registrations: Vec<ActivatedStoreDeviceRegistrationRef>,
1482 signer: &UserKeypair,
1483 ) -> Result<Self, StoreProtocolError> {
1484 Self::signed_operations(
1485 store_root_hash,
1486 write_id,
1487 coord,
1488 author_registration,
1489 author,
1490 order,
1491 membership_state,
1492 device_state,
1493 membership_authority,
1494 StoreCommitOperationsInput {
1495 control: None,
1496 device_join_attempts: Vec::new(),
1497 device_join_outcomes,
1498 device_join_abandonments: Vec::new(),
1499 device_join_cleanup_receipts: Vec::new(),
1500 provider_access_grants: Vec::new(),
1501 provider_access_withdrawals: Vec::new(),
1502 device_registrations,
1503 circle_controls: Vec::new(),
1504 store_package: None,
1505 circle_packages: &[],
1506 },
1507 signer,
1508 )
1509 }
1510
1511 #[allow(clippy::too_many_arguments)]
1512 pub fn signed_with_join_abandonments(
1513 store_root_hash: ObjectHash,
1514 write_id: WriteId,
1515 coord: StoreCommitCoord,
1516 author_registration: StoreDeviceRegistrationRef,
1517 author: &StoreDeviceRegistration,
1518 order: StoreCommitOrder,
1519 membership_state: StoreMembershipStateRef,
1520 device_state: StoreDeviceStateRef,
1521 membership_authority: Option<MembershipGrantCreationAuthority>,
1522 abandonments: Vec<super::device_join::DeviceJoinAbandonmentRef>,
1523 signer: &UserKeypair,
1524 ) -> Result<Self, StoreProtocolError> {
1525 Self::signed_operations(
1526 store_root_hash,
1527 write_id,
1528 coord,
1529 author_registration,
1530 author,
1531 order,
1532 membership_state,
1533 device_state,
1534 membership_authority,
1535 StoreCommitOperationsInput {
1536 control: None,
1537 device_join_attempts: Vec::new(),
1538 device_join_outcomes: Vec::new(),
1539 device_join_abandonments: abandonments,
1540 device_join_cleanup_receipts: Vec::new(),
1541 provider_access_grants: Vec::new(),
1542 provider_access_withdrawals: Vec::new(),
1543 device_registrations: Vec::new(),
1544 circle_controls: Vec::new(),
1545 store_package: None,
1546 circle_packages: &[],
1547 },
1548 signer,
1549 )
1550 }
1551
1552 #[allow(clippy::too_many_arguments)]
1553 pub fn signed_with_join_cleanup_receipts(
1554 store_root_hash: ObjectHash,
1555 write_id: WriteId,
1556 coord: StoreCommitCoord,
1557 author_registration: StoreDeviceRegistrationRef,
1558 author: &StoreDeviceRegistration,
1559 order: StoreCommitOrder,
1560 membership_state: StoreMembershipStateRef,
1561 device_state: StoreDeviceStateRef,
1562 membership_authority: Option<MembershipGrantCreationAuthority>,
1563 receipts: Vec<super::device_join::DeviceJoinCleanupReceiptRef>,
1564 signer: &UserKeypair,
1565 ) -> Result<Self, StoreProtocolError> {
1566 Self::signed_operations(
1567 store_root_hash,
1568 write_id,
1569 coord,
1570 author_registration,
1571 author,
1572 order,
1573 membership_state,
1574 device_state,
1575 membership_authority,
1576 StoreCommitOperationsInput {
1577 control: None,
1578 device_join_attempts: Vec::new(),
1579 device_join_outcomes: Vec::new(),
1580 device_join_abandonments: Vec::new(),
1581 device_join_cleanup_receipts: receipts,
1582 provider_access_grants: Vec::new(),
1583 provider_access_withdrawals: Vec::new(),
1584 device_registrations: Vec::new(),
1585 circle_controls: Vec::new(),
1586 store_package: None,
1587 circle_packages: &[],
1588 },
1589 signer,
1590 )
1591 }
1592
1593 #[allow(clippy::too_many_arguments)]
1594 pub fn signed_with_provider_access(
1595 store_root_hash: ObjectHash,
1596 write_id: WriteId,
1597 coord: StoreCommitCoord,
1598 author_registration: StoreDeviceRegistrationRef,
1599 author: &StoreDeviceRegistration,
1600 order: StoreCommitOrder,
1601 membership_state: StoreMembershipStateRef,
1602 device_state: StoreDeviceStateRef,
1603 membership_authority: Option<MembershipGrantCreationAuthority>,
1604 provider_access_grants: Vec<super::provider::StoreMemberProviderAccessGrantRef>,
1605 provider_access_withdrawals: Vec<
1606 super::provider::StoreMemberProviderAccessWithdrawalReceiptRef,
1607 >,
1608 signer: &UserKeypair,
1609 ) -> Result<Self, StoreProtocolError> {
1610 Self::signed_operations(
1611 store_root_hash,
1612 write_id,
1613 coord,
1614 author_registration,
1615 author,
1616 order,
1617 membership_state,
1618 device_state,
1619 membership_authority,
1620 StoreCommitOperationsInput {
1621 control: None,
1622 device_join_attempts: Vec::new(),
1623 device_join_outcomes: Vec::new(),
1624 device_join_abandonments: Vec::new(),
1625 device_join_cleanup_receipts: Vec::new(),
1626 provider_access_grants,
1627 provider_access_withdrawals,
1628 device_registrations: Vec::new(),
1629 circle_controls: Vec::new(),
1630 store_package: None,
1631 circle_packages: &[],
1632 },
1633 signer,
1634 )
1635 }
1636
1637 #[allow(clippy::too_many_arguments)]
1638 pub fn signed_operations(
1639 store_root_hash: ObjectHash,
1640 write_id: WriteId,
1641 coord: StoreCommitCoord,
1642 author_registration: StoreDeviceRegistrationRef,
1643 author: &StoreDeviceRegistration,
1644 order: StoreCommitOrder,
1645 membership_state: StoreMembershipStateRef,
1646 device_state: StoreDeviceStateRef,
1647 membership_authority: Option<MembershipGrantCreationAuthority>,
1648 input: StoreCommitOperationsInput<'_>,
1649 signer: &UserKeypair,
1650 ) -> Result<Self, StoreProtocolError> {
1651 validate_commit_envelope(
1652 store_root_hash,
1653 &coord,
1654 &author_registration,
1655 author,
1656 &order,
1657 &membership_state,
1658 &device_state,
1659 membership_authority.as_ref(),
1660 signer,
1661 )?;
1662 let StoreCommitOperationsInput {
1663 control,
1664 device_join_attempts,
1665 device_join_outcomes,
1666 device_join_abandonments,
1667 device_join_cleanup_receipts,
1668 provider_access_grants,
1669 provider_access_withdrawals,
1670 device_registrations,
1671 circle_controls,
1672 store_package,
1673 circle_packages,
1674 } = input;
1675 validate_control(
1676 order.policy(),
1677 store_root_hash,
1678 &author.author_pubkey,
1679 control.as_ref(),
1680 )?;
1681 let stream_id = commit_stream_id(&coord);
1682 let seq = order.seq();
1683 let candidate_family =
1684 CandidateFamilyId::derive(store_root_hash, &author_registration, &write_id, &order);
1685 let store_package = store_package
1686 .map(|input| {
1687 if input.candidate_family != candidate_family {
1688 return Err(StoreProtocolError::Malformed(
1689 "Store package candidate family differs from its commit".to_string(),
1690 ));
1691 }
1692 let semantic_prefix = package_semantic_prefix(
1693 candidate_family,
1694 &stream_id,
1695 seq,
1696 ObjectHash::digest(input.bytes),
1697 );
1698 package_ref(&semantic_prefix, &input)
1699 })
1700 .transpose()?;
1701 validate_device_join_attempt_refs(&device_join_attempts)?;
1702 validate_device_join_outcome_refs(&device_join_outcomes)?;
1703 validate_device_join_abandonment_refs(&device_join_abandonments)?;
1704 validate_device_join_cleanup_receipt_refs(&device_join_cleanup_receipts)?;
1705 validate_provider_access_refs(&provider_access_grants, &provider_access_withdrawals)?;
1706 validate_device_registration_refs(&device_registrations)?;
1707 let mut seen_circles = BTreeSet::new();
1708 let circle_packages = circle_packages
1709 .iter()
1710 .map(|input| {
1711 if !seen_circles.insert(input.circle_id) {
1712 return Err(StoreProtocolError::DuplicateCirclePackage(input.circle_id));
1713 }
1714 validate_circle_control_coord(order.policy(), &input.control)?;
1715 if input.package.candidate_family != candidate_family {
1716 return Err(StoreProtocolError::Malformed(
1717 "Circle package candidate family differs from its commit".to_string(),
1718 ));
1719 }
1720 let semantic_prefix = circle_package_semantic_prefix(
1721 input.circle_id,
1722 candidate_family,
1723 &stream_id,
1724 seq,
1725 ObjectHash::digest(input.package.bytes),
1726 );
1727 let package = package_ref(&semantic_prefix, &input.package)?;
1728 Ok(CirclePackageRef {
1729 circle_id: input.circle_id,
1730 control: input.control.clone(),
1731 package,
1732 key_fingerprint: input.key_fingerprint,
1733 })
1734 })
1735 .collect::<Result<Vec<_>, StoreProtocolError>>()?;
1736 validate_circle_control_refs(order.policy(), &circle_controls)?;
1737 let operations = StoreCommitOperations {
1738 control,
1739 device_join_attempts,
1740 device_join_outcomes,
1741 device_join_abandonments,
1742 device_join_cleanup_receipts,
1743 provider_access_grants,
1744 provider_access_withdrawals,
1745 device_registrations,
1746 circle_controls,
1747 store_package,
1748 circle_packages,
1749 };
1750 if operations.is_empty() {
1751 return Err(StoreProtocolError::EmptyBatch);
1752 }
1753 Self::finish_signed_body(
1754 store_root_hash,
1755 write_id,
1756 author_registration,
1757 order,
1758 membership_state,
1759 device_state,
1760 membership_authority,
1761 StoreCommitBody::Operations(operations),
1762 signer,
1763 )
1764 }
1765
1766 #[allow(clippy::too_many_arguments)]
1767 fn finish_signed_body(
1768 store_root_hash: ObjectHash,
1769 write_id: WriteId,
1770 author_registration: StoreDeviceRegistrationRef,
1771 order: StoreCommitOrder,
1772 membership_state: StoreMembershipStateRef,
1773 device_state: StoreDeviceStateRef,
1774 membership_authority: Option<MembershipGrantCreationAuthority>,
1775 body: StoreCommitBody,
1776 signer: &UserKeypair,
1777 ) -> Result<Self, StoreProtocolError> {
1778 let family =
1779 CandidateFamilyId::derive(store_root_hash, &author_registration, &write_id, &order);
1780 validate_commit_body(&body, family, &author_registration, &order)?;
1781 let candidate_objects = candidate_manifest(family, &body)?;
1782 let mut commit = Self {
1783 version: STORE_PROTOCOL_VERSION,
1784 store_root_hash,
1785 write_id,
1786 author_registration,
1787 order,
1788 membership_state,
1789 device_state,
1790 membership_authority,
1791 candidate_objects,
1792 body,
1793 signature: String::new(),
1794 };
1795 let (_, signature) = keys::sign_hex(signer, &commit.canonical_signed_bytes());
1796 commit.signature = signature;
1797 Ok(commit)
1798 }
1799
1800 pub fn canonical_signed_bytes(&self) -> Vec<u8> {
1801 let fields = CommitSignedFields {
1802 version: self.version,
1803 store_root_hash: self.store_root_hash,
1804 write_id: &self.write_id,
1805 author_registration: &self.author_registration,
1806 order: &self.order,
1807 membership_state: &self.membership_state,
1808 device_state: &self.device_state,
1809 membership_authority: self.membership_authority.as_ref(),
1810 candidate_objects: &self.candidate_objects,
1811 body: &self.body,
1812 };
1813 domain_json(COMMIT_DOMAIN, &fields)
1814 }
1815
1816 pub fn commit_hash(&self) -> ObjectHash {
1817 ObjectHash::digest(&self.canonical_signed_bytes())
1818 }
1819
1820 pub fn to_bytes(&self) -> Vec<u8> {
1821 serde_json::to_vec(self).expect("StoreBatchCommit serialization cannot fail")
1822 }
1823
1824 pub fn parse_at(
1825 bytes: &[u8],
1826 expected_store_root_hash: ObjectHash,
1827 expected_coord: &StoreCommitCoord,
1828 author: &StoreDeviceRegistration,
1829 ) -> Result<Self, StoreProtocolError> {
1830 let commit: Self = serde_json::from_slice(bytes)
1831 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
1832 commit.verify_at(expected_store_root_hash, expected_coord, author)?;
1833 Ok(commit)
1834 }
1835
1836 pub fn verify_at(
1837 &self,
1838 expected_store_root_hash: ObjectHash,
1839 expected_coord: &StoreCommitCoord,
1840 author: &StoreDeviceRegistration,
1841 ) -> Result<(), StoreProtocolError> {
1842 require_version(self.version)?;
1843 if self.store_root_hash != expected_store_root_hash {
1844 return Err(StoreProtocolError::StoreRootMismatch {
1845 expected: expected_store_root_hash,
1846 actual: self.store_root_hash,
1847 });
1848 }
1849 if self.order.policy() != expected_coord.policy() {
1850 return Err(StoreProtocolError::WritePolicyMismatch {
1851 expected: expected_coord.policy(),
1852 actual: self.order.policy(),
1853 });
1854 }
1855 let stream_id = commit_stream_id(expected_coord);
1856 if self.order.seq() != expected_coord.sequence() {
1857 return Err(StoreProtocolError::RelocatedSlot {
1858 expected: commit_slot_prefix(&stream_id, expected_coord.sequence()),
1859 actual: commit_slot_prefix(&stream_id, self.order.seq()),
1860 });
1861 }
1862 self.author_registration.verify_registration(author)?;
1863 let family = self.candidate_family();
1864 if let Some(package) = self.store_package() {
1865 if package.candidate_family != self.candidate_family() {
1866 return Err(StoreProtocolError::Malformed(
1867 "Store package candidate family differs from its commit".to_string(),
1868 ));
1869 }
1870 let expected =
1871 package_semantic_prefix(family, &stream_id, self.order.seq(), package.content_hash);
1872 if package.object.slot().logical_key() != format!("{expected}.pkg") {
1873 return Err(StoreProtocolError::RelocatedPackage {
1874 expected,
1875 actual: package.object.slot().logical_key().to_string(),
1876 });
1877 }
1878 }
1879 let mut seen_circles = BTreeSet::new();
1880 for circle_package in self.circle_packages() {
1881 if circle_package.package.candidate_family != self.candidate_family() {
1882 return Err(StoreProtocolError::Malformed(
1883 "Circle package candidate family differs from its commit".to_string(),
1884 ));
1885 }
1886 if !seen_circles.insert(circle_package.circle_id) {
1887 return Err(StoreProtocolError::DuplicateCirclePackage(
1888 circle_package.circle_id,
1889 ));
1890 }
1891 validate_circle_control_coord(self.policy(), &circle_package.control)?;
1892 let expected = circle_package_semantic_prefix(
1893 circle_package.circle_id,
1894 family,
1895 &stream_id,
1896 self.seq(),
1897 circle_package.package.content_hash,
1898 );
1899 if circle_package.package.object.slot().logical_key() != format!("{expected}.pkg") {
1900 return Err(StoreProtocolError::RelocatedCirclePackage {
1901 circle_id: circle_package.circle_id,
1902 expected,
1903 actual: circle_package
1904 .package
1905 .object
1906 .slot()
1907 .logical_key()
1908 .to_string(),
1909 });
1910 }
1911 }
1912 validate_commit_body(&self.body, family, &self.author_registration, &self.order)?;
1913 if let StoreCommitBody::AbandonCandidates { manifests } = &self.body {
1914 validate_candidate_abandonment(
1915 manifests,
1916 self.store_root_hash,
1917 &self.author_registration,
1918 expected_coord,
1919 &self.order,
1920 author,
1921 )?;
1922 }
1923 if self.candidate_objects != candidate_manifest(family, &self.body)? {
1924 return Err(StoreProtocolError::Malformed(
1925 "candidate object manifest differs from the exact commit body graph".to_string(),
1926 ));
1927 }
1928 validate_commit_order(&self.order)?;
1929 validate_commit_predecessor_states(
1930 &self.order,
1931 &self.membership_state,
1932 &self.device_state,
1933 )?;
1934 if let Some(authority) = self.membership_authority.as_ref() {
1935 validate_membership_authority(authority)?;
1936 }
1937 validate_parsed_control(self, author)?;
1938 if !keys::verify_signature_hex(
1939 &author.device_signing_pubkey,
1940 &self.signature,
1941 &self.canonical_signed_bytes(),
1942 ) {
1943 return Err(StoreProtocolError::InvalidSignature);
1944 }
1945 Ok(())
1946 }
1947
1948 pub fn verify_store_package(&self, package_bytes: &[u8]) -> Result<(), StoreProtocolError> {
1949 let package = self
1950 .store_package()
1951 .ok_or(StoreProtocolError::MissingStorePackage)?;
1952 verify_package_ref(package, package_bytes)
1953 }
1954
1955 pub fn verify_circle_package(
1956 &self,
1957 circle_id: CircleId,
1958 package_bytes: &[u8],
1959 ) -> Result<(), StoreProtocolError> {
1960 let package = self
1961 .circle_packages()
1962 .iter()
1963 .find(|package| package.circle_id == circle_id)
1964 .ok_or(StoreProtocolError::MissingCirclePackage(circle_id))?;
1965 verify_package_ref(&package.package, package_bytes)
1966 }
1967}
1968
1969#[allow(clippy::too_many_arguments)]
1970fn validate_commit_envelope(
1971 store_root_hash: ObjectHash,
1972 coord: &StoreCommitCoord,
1973 author_registration: &StoreDeviceRegistrationRef,
1974 author: &StoreDeviceRegistration,
1975 order: &StoreCommitOrder,
1976 membership_state: &StoreMembershipStateRef,
1977 device_state: &StoreDeviceStateRef,
1978 membership_authority: Option<&MembershipGrantCreationAuthority>,
1979 signer: &UserKeypair,
1980) -> Result<(), StoreProtocolError> {
1981 author_registration.verify_registration(author)?;
1982 if keys::public_key_hex(signer) != author.device_signing_pubkey {
1983 return Err(StoreProtocolError::InvalidSignature);
1984 }
1985 if order.seq() == 0 {
1986 return Err(StoreProtocolError::InvalidSequence(0));
1987 }
1988 validate_commit_order(order)?;
1989 validate_commit_predecessor_states(order, membership_state, device_state)?;
1990 if coord.sequence() != order.seq() || coord.policy() != order.policy() {
1991 return Err(StoreProtocolError::Malformed(
1992 "Store commit coordinate disagrees with its order".to_string(),
1993 ));
1994 }
1995 if let Some(authority) = membership_authority {
1996 validate_membership_authority(authority)?;
1997 }
1998 if store_root_hash != author.store_root.store_root_hash {
1999 return Err(StoreProtocolError::StoreRootMismatch {
2000 expected: store_root_hash,
2001 actual: author.store_root.store_root_hash,
2002 });
2003 }
2004 Ok(())
2005}
2006
2007fn validate_commit_body(
2008 body: &StoreCommitBody,
2009 family: CandidateFamilyId,
2010 author: &StoreDeviceRegistrationRef,
2011 order: &StoreCommitOrder,
2012) -> Result<(), StoreProtocolError> {
2013 match body {
2014 StoreCommitBody::Operations(operations) => {
2015 if operations.is_empty() {
2016 return Err(StoreProtocolError::EmptyBatch);
2017 }
2018 validate_circle_control_refs(order.policy(), &operations.circle_controls)?;
2019 validate_device_join_attempt_refs(&operations.device_join_attempts)?;
2020 validate_device_join_outcome_refs(&operations.device_join_outcomes)?;
2021 validate_device_join_abandonment_refs(&operations.device_join_abandonments)?;
2022 validate_device_join_cleanup_receipt_refs(&operations.device_join_cleanup_receipts)?;
2023 validate_provider_access_refs(
2024 &operations.provider_access_grants,
2025 &operations.provider_access_withdrawals,
2026 )?;
2027 validate_device_registration_refs(&operations.device_registrations)?;
2028 }
2029 StoreCommitBody::SelfRetirement { retirement } => {
2030 validate_device_retirement_refs(
2031 std::slice::from_ref(retirement),
2032 family,
2033 author,
2034 order,
2035 )?;
2036 }
2037 StoreCommitBody::SerialRecoveryActivation { activation } => {
2038 validate_serial_recovery_activation(order, activation, author)?;
2039 }
2040 StoreCommitBody::AbandonCandidates { manifests } => {
2041 if manifests.is_empty() {
2042 return Err(StoreProtocolError::Malformed(
2043 "candidate abandonment has no candidates".to_string(),
2044 ));
2045 }
2046 }
2047 }
2048 Ok(())
2049}
2050
2051fn validate_candidate_abandonment(
2052 manifests: &[CandidateCleanupManifest],
2053 store_root_hash: ObjectHash,
2054 author_registration: &StoreDeviceRegistrationRef,
2055 coord: &StoreCommitCoord,
2056 order: &StoreCommitOrder,
2057 author: &StoreDeviceRegistration,
2058) -> Result<(), StoreProtocolError> {
2059 if manifests.is_empty() {
2060 return Err(StoreProtocolError::Malformed(
2061 "candidate abandonment has no candidates".to_string(),
2062 ));
2063 }
2064 if manifests.windows(2).any(|pair| pair[0] >= pair[1]) {
2065 return Err(StoreProtocolError::Malformed(
2066 "candidate abandonment manifests are not strictly sorted and unique".to_string(),
2067 ));
2068 }
2069 for manifest in manifests {
2070 if &manifest.candidate.coord != coord {
2071 return Err(StoreProtocolError::Malformed(
2072 "abandoned candidate occupies a different competition point".to_string(),
2073 ));
2074 }
2075 let candidate = manifest
2076 .candidate
2077 .verify_candidate(store_root_hash, author)?;
2078 if &candidate.author_registration != author_registration {
2079 return Err(StoreProtocolError::Malformed(
2080 "abandoned candidate has a different author registration".to_string(),
2081 ));
2082 }
2083 let shares_predecessor = match (&candidate.order, order) {
2084 (
2085 StoreCommitOrder::MergeConcurrent {
2086 predecessor: candidate_predecessor,
2087 ..
2088 },
2089 StoreCommitOrder::MergeConcurrent { predecessor, .. },
2090 ) => candidate_predecessor == predecessor,
2091 (
2092 StoreCommitOrder::Serial {
2093 predecessor: candidate_predecessor,
2094 ..
2095 },
2096 StoreCommitOrder::Serial { predecessor, .. },
2097 ) => candidate_predecessor == predecessor,
2098 _ => false,
2099 };
2100 if !shares_predecessor {
2101 return Err(StoreProtocolError::Malformed(
2102 "abandoned candidate has a different predecessor".to_string(),
2103 ));
2104 }
2105 }
2106 Ok(())
2107}
2108
2109fn candidate_manifest(
2110 family: CandidateFamilyId,
2111 body: &StoreCommitBody,
2112) -> Result<CandidateObjectManifest, StoreProtocolError> {
2113 let mut objects = Vec::new();
2114 match body {
2115 StoreCommitBody::Operations(operations) => {
2116 objects.extend(
2117 operations
2118 .store_package
2119 .iter()
2120 .cloned()
2121 .map(CandidateExclusiveObjectRef::StorePackage),
2122 );
2123 objects.extend(
2124 operations
2125 .circle_packages
2126 .iter()
2127 .cloned()
2128 .map(CandidateExclusiveObjectRef::CirclePackage),
2129 );
2130 for control in &operations.circle_controls {
2131 let circle_id = control.circle_id();
2132 if control
2133 .objects()
2134 .access
2135 .iter()
2136 .any(|access| access.envelope.control_hash != control.control().control_hash())
2137 {
2138 return Err(StoreProtocolError::Malformed(
2139 "Circle access envelope differs from its activating control".to_string(),
2140 ));
2141 }
2142 objects.extend(
2143 control.objects().access.iter().cloned().map(|access| {
2144 CandidateExclusiveObjectRef::CircleAccess { circle_id, access }
2145 }),
2146 );
2147 }
2148 }
2149 StoreCommitBody::SelfRetirement { retirement } => {
2150 objects.push(CandidateExclusiveObjectRef::SelfRetirement(
2151 retirement.clone(),
2152 ));
2153 }
2154 StoreCommitBody::SerialRecoveryActivation { .. }
2155 | StoreCommitBody::AbandonCandidates { .. } => {}
2156 }
2157 objects.sort_by_cached_key(|object| {
2158 serde_json::to_vec(object).expect("candidate object serialization cannot fail")
2159 });
2160 let mut exact_refs = BTreeSet::new();
2161 let mut access_keys = BTreeSet::new();
2162 for object in &objects {
2163 validate_candidate_object_path(family, object)?;
2164 match object {
2165 CandidateExclusiveObjectRef::CircleAccess { circle_id, access } => {
2166 let key = (
2167 *circle_id,
2168 access.leaf.owner_pubkey.clone(),
2169 access.leaf.recipient_slot.clone(),
2170 access.envelope.control_hash,
2171 );
2172 if !access_keys.insert(key) {
2173 return Err(StoreProtocolError::Malformed(
2174 "candidate object manifest repeats a Circle access semantic key"
2175 .to_string(),
2176 ));
2177 }
2178 insert_candidate_exact_ref(&mut exact_refs, &access.leaf.object)?;
2179 insert_candidate_exact_ref(&mut exact_refs, &access.envelope.object)?;
2180 }
2181 CandidateExclusiveObjectRef::StorePackage(reference) => {
2182 insert_candidate_exact_ref(&mut exact_refs, &reference.object)?;
2183 }
2184 CandidateExclusiveObjectRef::CirclePackage(reference) => {
2185 insert_candidate_exact_ref(&mut exact_refs, &reference.package.object)?;
2186 }
2187 CandidateExclusiveObjectRef::SelfRetirement(reference) => {
2188 insert_candidate_exact_ref(&mut exact_refs, &reference.object)?;
2189 }
2190 }
2191 }
2192 Ok(CandidateObjectManifest { family, objects })
2193}
2194
2195fn insert_candidate_exact_ref<'a>(
2196 exact_refs: &mut BTreeSet<&'a ExactObjectRef>,
2197 object: &'a ExactObjectRef,
2198) -> Result<(), StoreProtocolError> {
2199 if !exact_refs.insert(object) {
2200 return Err(StoreProtocolError::Malformed(
2201 "candidate object manifest repeats an exact object reference".to_string(),
2202 ));
2203 }
2204 Ok(())
2205}
2206
2207fn validate_candidate_object_path(
2208 family: CandidateFamilyId,
2209 candidate: &CandidateExclusiveObjectRef,
2210) -> Result<(), StoreProtocolError> {
2211 let (expected, object) = match candidate {
2212 CandidateExclusiveObjectRef::StorePackage(reference) => {
2213 if reference.candidate_family != family {
2214 return Err(StoreProtocolError::Malformed(
2215 "Store package candidate family differs from its manifest".to_string(),
2216 ));
2217 }
2218 return Ok(());
2219 }
2220 CandidateExclusiveObjectRef::CirclePackage(reference) => {
2221 if reference.package.candidate_family != family {
2222 return Err(StoreProtocolError::Malformed(
2223 "Circle package candidate family differs from its manifest".to_string(),
2224 ));
2225 }
2226 return Ok(());
2227 }
2228 CandidateExclusiveObjectRef::CircleAccess { circle_id, access } => {
2229 validate_circle_access_ref(*circle_id, family, access)?;
2230 return Ok(());
2231 }
2232 CandidateExclusiveObjectRef::SelfRetirement(reference) => (
2233 format!(
2234 "{}.json",
2235 device_self_retirement_semantic_prefix(
2236 family,
2237 &reference.target.device_id,
2238 reference.retirement_hash,
2239 )
2240 ),
2241 &reference.object,
2242 ),
2243 };
2244 if object.slot().logical_key() != expected {
2245 return Err(StoreProtocolError::RelocatedCandidateObject {
2246 expected,
2247 actual: object.slot().logical_key().to_string(),
2248 });
2249 }
2250 Ok(())
2251}
2252
2253fn validate_circle_access_ref(
2254 circle_id: CircleId,
2255 family: CandidateFamilyId,
2256 access: &CircleAccessObjectRef,
2257) -> Result<(), StoreProtocolError> {
2258 if access.leaf.owner_pubkey != access.envelope.owner_pubkey
2259 || access.leaf.recipient_slot != access.envelope.recipient_slot
2260 || access.leaf.leaf_id != access.envelope.leaf_id
2261 || access.leaf.leaf_hash != access.envelope.leaf_hash
2262 || access.leaf.leaf_hash != access.leaf.object.stored_hash()
2263 {
2264 return Err(StoreProtocolError::Malformed(
2265 "paired Circle access leaf and envelope references differ".to_string(),
2266 ));
2267 }
2268 let leaf_expected = circle_access_leaf_semantic_prefix(
2269 circle_id,
2270 family,
2271 &access.leaf.owner_pubkey,
2272 access.leaf.epoch_id,
2273 &access.leaf.recipient_slot,
2274 access.leaf.leaf_id,
2275 );
2276 if access.leaf.object.slot().logical_key() != leaf_expected {
2277 return Err(StoreProtocolError::RelocatedCandidateObject {
2278 expected: leaf_expected,
2279 actual: access.leaf.object.slot().logical_key().to_string(),
2280 });
2281 }
2282 let envelope_expected = format!(
2283 "{}.json",
2284 circle_access_envelope_semantic_prefix(
2285 circle_id,
2286 family,
2287 &access.envelope.owner_pubkey,
2288 &access.envelope.recipient_slot,
2289 access.envelope.control_hash,
2290 )
2291 );
2292 if access.envelope.object.slot().logical_key() != envelope_expected {
2293 return Err(StoreProtocolError::RelocatedCandidateObject {
2294 expected: envelope_expected,
2295 actual: access.envelope.object.slot().logical_key().to_string(),
2296 });
2297 }
2298 Ok(())
2299}
2300
2301fn package_ref(
2302 semantic_prefix: &str,
2303 input: &StorePackageInput<'_>,
2304) -> Result<StorePackageRef, StoreProtocolError> {
2305 let package_bytes = input.bytes;
2306 let changeset_size =
2307 u64::try_from(package_bytes.len()).map_err(|_| StoreProtocolError::PackageTooLarge)?;
2308 let content_hash = ObjectHash::digest(package_bytes);
2309 let expected_key = format!("{semantic_prefix}.pkg");
2310 if input.object.slot().logical_key() != expected_key {
2311 return Err(StoreProtocolError::RelocatedPackage {
2312 expected: expected_key,
2313 actual: input.object.slot().logical_key().to_string(),
2314 });
2315 }
2316 Ok(StorePackageRef {
2317 candidate_family: input.candidate_family,
2318 content_hash,
2319 schema_version: input.schema_version,
2320 changeset_size,
2321 object: input.object.clone(),
2322 })
2323}
2324
2325fn verify_package_ref(
2326 package: &StorePackageRef,
2327 package_bytes: &[u8],
2328) -> Result<(), StoreProtocolError> {
2329 let length =
2330 u64::try_from(package_bytes.len()).map_err(|_| StoreProtocolError::PackageTooLarge)?;
2331 if length != package.changeset_size {
2332 return Err(StoreProtocolError::PackageLengthMismatch {
2333 expected: package.changeset_size,
2334 actual: length,
2335 });
2336 }
2337 let actual = ObjectHash::digest(package_bytes);
2338 if actual != package.content_hash {
2339 return Err(StoreProtocolError::PackageHashMismatch {
2340 expected: package.content_hash,
2341 actual,
2342 });
2343 }
2344 Ok(())
2345}
2346
2347fn validate_control(
2348 policy: WritePolicy,
2349 store_root_hash: ObjectHash,
2350 author_pubkey: &str,
2351 control: Option<&StoreControl>,
2352) -> Result<(), StoreProtocolError> {
2353 let Some(control) = control else {
2354 return Ok(());
2355 };
2356 if policy != WritePolicy::Serial {
2357 return Err(StoreProtocolError::ControlRequiresSerial);
2358 }
2359 if let Some(entry) = control.serial_membership_entry() {
2360 if entry.store_root_hash != store_root_hash
2361 || entry.author_pubkey != author_pubkey
2362 || !entry.verify()
2363 {
2364 return Err(StoreProtocolError::InvalidSerialControl);
2365 }
2366 }
2367 if control.key_generation() == Some(0) {
2368 return Err(StoreProtocolError::InvalidKeyGeneration(0));
2369 }
2370 Ok(())
2371}
2372
2373fn validate_serial_recovery_activation(
2374 order: &StoreCommitOrder,
2375 activation: &SerialRecoveryActivation,
2376 author_registration: &StoreDeviceRegistrationRef,
2377) -> Result<(), StoreProtocolError> {
2378 if order.policy() != WritePolicy::Serial {
2379 return Err(StoreProtocolError::WritePolicyMismatch {
2380 expected: WritePolicy::Serial,
2381 actual: order.policy(),
2382 });
2383 }
2384 if &activation.registration.registration != author_registration
2385 || !matches!(
2386 activation.registration.authority,
2387 StoreDeviceRegistrationActivationRef::Recovery { .. }
2388 )
2389 {
2390 return Err(StoreProtocolError::Malformed(
2391 "Serial recovery activation does not bind its Recovery author registration".to_string(),
2392 ));
2393 }
2394 Ok(())
2395}
2396
2397fn validate_parsed_control(
2398 commit: &StoreBatchCommit,
2399 author: &StoreDeviceRegistration,
2400) -> Result<(), StoreProtocolError> {
2401 validate_control(
2402 commit.policy(),
2403 commit.store_root_hash,
2404 &author.author_pubkey,
2405 commit.control(),
2406 )
2407}
2408
2409fn validate_circle_control_coord(
2410 policy: WritePolicy,
2411 coord: &CircleControlCoord,
2412) -> Result<(), StoreProtocolError> {
2413 coord
2414 .validate()
2415 .map_err(|_| StoreProtocolError::InvalidCircleControlCoord)?;
2416 let actual = match coord {
2417 CircleControlCoord::MergeConcurrent { .. } => WritePolicy::MergeConcurrent,
2418 CircleControlCoord::Serial { .. } => WritePolicy::Serial,
2419 };
2420 if policy != actual {
2421 return Err(StoreProtocolError::CircleControlPolicyMismatch {
2422 expected: policy,
2423 actual,
2424 });
2425 }
2426 Ok(())
2427}
2428
2429fn validate_circle_control_refs(
2430 policy: WritePolicy,
2431 controls: &[CircleControlRef],
2432) -> Result<(), StoreProtocolError> {
2433 let mut seen = BTreeSet::new();
2434 for control_ref in controls {
2435 if !seen.insert(control_ref.circle_id()) {
2436 return Err(StoreProtocolError::DuplicateCircleControl(
2437 control_ref.circle_id(),
2438 ));
2439 }
2440 validate_circle_control_coord(policy, control_ref.control())?;
2441 if matches!(policy, WritePolicy::MergeConcurrent)
2442 != matches!(control_ref, CircleControlRef::MergeConcurrent { .. })
2443 {
2444 return Err(StoreProtocolError::CircleControlPolicyMismatch {
2445 expected: policy,
2446 actual: match control_ref {
2447 CircleControlRef::MergeConcurrent { .. } => WritePolicy::MergeConcurrent,
2448 CircleControlRef::Serial { .. } => WritePolicy::Serial,
2449 },
2450 });
2451 }
2452 }
2453 Ok(())
2454}
2455
2456fn validate_device_registration_refs(
2457 registrations: &[ActivatedStoreDeviceRegistrationRef],
2458) -> Result<(), StoreProtocolError> {
2459 let mut seen = BTreeSet::new();
2460 for activation in registrations {
2461 if !seen.insert(activation.registration.device_id) {
2462 return Err(StoreProtocolError::DuplicateDeviceRegistration {
2463 device_id: activation.registration.device_id.to_string(),
2464 revision: 1,
2465 });
2466 }
2467 }
2468 Ok(())
2469}
2470
2471fn validate_device_join_attempt_refs(
2472 attempts: &[DeviceJoinAttemptRef],
2473) -> Result<(), StoreProtocolError> {
2474 if attempts.windows(2).any(|pair| pair[0] >= pair[1]) {
2475 return Err(StoreProtocolError::JoinAttemptMismatch);
2476 }
2477 Ok(())
2478}
2479
2480fn validate_device_join_outcome_refs(
2481 outcomes: &[DeviceJoinOutcomeRef],
2482) -> Result<(), StoreProtocolError> {
2483 let mut attempts = BTreeSet::new();
2484 if outcomes.windows(2).any(|pair| pair[0] >= pair[1])
2485 || outcomes
2486 .iter()
2487 .any(|outcome| !attempts.insert(outcome.attempt().attempt_id))
2488 {
2489 return Err(StoreProtocolError::JoinOutcomeMismatch);
2490 }
2491 Ok(())
2492}
2493
2494fn validate_device_join_abandonment_refs(
2495 abandonments: &[super::device_join::DeviceJoinAbandonmentRef],
2496) -> Result<(), StoreProtocolError> {
2497 if abandonments.windows(2).any(|pair| pair[0] >= pair[1]) {
2498 return Err(StoreProtocolError::JoinOutcomeMismatch);
2499 }
2500 Ok(())
2501}
2502
2503fn validate_device_join_cleanup_receipt_refs(
2504 receipts: &[super::device_join::DeviceJoinCleanupReceiptRef],
2505) -> Result<(), StoreProtocolError> {
2506 if receipts.windows(2).any(|pair| pair[0] >= pair[1]) {
2507 return Err(StoreProtocolError::JoinOutcomeMismatch);
2508 }
2509 Ok(())
2510}
2511
2512fn validate_provider_access_refs(
2513 grants: &[super::provider::StoreMemberProviderAccessGrantRef],
2514 withdrawals: &[super::provider::StoreMemberProviderAccessWithdrawalReceiptRef],
2515) -> Result<(), StoreProtocolError> {
2516 if grants.windows(2).any(|pair| pair[0] >= pair[1])
2517 || withdrawals.windows(2).any(|pair| pair[0] >= pair[1])
2518 {
2519 return Err(StoreProtocolError::ProviderAccessMismatch);
2520 }
2521 let granted = grants
2522 .iter()
2523 .map(|reference| &reference.grant_id)
2524 .collect::<BTreeSet<_>>();
2525 if withdrawals
2526 .iter()
2527 .any(|reference| granted.contains(&reference.grant_id))
2528 {
2529 return Err(StoreProtocolError::ProviderAccessMismatch);
2530 }
2531 Ok(())
2532}
2533
2534fn validate_device_retirement_refs(
2535 retirements: &[StoreDeviceSelfRetirementRef],
2536 candidate_family: CandidateFamilyId,
2537 author: &StoreDeviceRegistrationRef,
2538 order: &StoreCommitOrder,
2539) -> Result<(), StoreProtocolError> {
2540 if retirements.len() > 1 {
2541 return Err(StoreProtocolError::DeviceStateMismatch);
2542 }
2543 let expected_cut = match order {
2544 StoreCommitOrder::MergeConcurrent {
2545 predecessor,
2546 dependencies,
2547 ..
2548 } => {
2549 let mut cut = dependencies.clone();
2550 if let Some(predecessor) = predecessor {
2551 let StoreCommitCoord::MergeConcurrent { stream_id, .. } = predecessor.coord else {
2552 return Err(StoreProtocolError::DeviceStateMismatch);
2553 };
2554 if cut
2555 .insert(stream_id, predecessor.clone())
2556 .is_some_and(|existing| existing != *predecessor)
2557 {
2558 return Err(StoreProtocolError::DeviceStateMismatch);
2559 }
2560 }
2561 StoreHistoryCut::MergeConcurrent(cut)
2562 }
2563 StoreCommitOrder::Serial { predecessor, .. } => {
2564 StoreHistoryCut::Serial(predecessor.clone())
2565 }
2566 };
2567 for retirement in retirements {
2568 if retirement.candidate_family != candidate_family
2569 || retirement.target != *author
2570 || retirement.retiring_cut != expected_cut
2571 {
2572 return Err(StoreProtocolError::DeviceStateMismatch);
2573 }
2574 }
2575 Ok(())
2576}
2577
2578#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2579#[serde(deny_unknown_fields)]
2580pub struct StoreDeviceHead {
2581 pub version: u32,
2582 pub store_root_hash: ObjectHash,
2583 pub author_registration: StoreDeviceRegistrationRef,
2584 pub commit: StoreBatchCommitRef,
2585 pub successor: SuccessorLink,
2586 pub signature: String,
2587}
2588
2589#[derive(Serialize)]
2590struct HeadSignedFields<'a> {
2591 version: u32,
2592 store_root_hash: ObjectHash,
2593 author_registration: &'a StoreDeviceRegistrationRef,
2594 commit: &'a StoreBatchCommitRef,
2595 successor: &'a SuccessorLink,
2596}
2597
2598impl StoreDeviceHead {
2599 pub fn signed(
2600 store_root_hash: ObjectHash,
2601 author_registration: StoreDeviceRegistrationRef,
2602 commit: StoreBatchCommitRef,
2603 successor: SuccessorLink,
2604 signer: &UserKeypair,
2605 ) -> Result<Self, StoreProtocolError> {
2606 if commit.coord.sequence() == 0 {
2607 return Err(StoreProtocolError::InvalidSequence(0));
2608 }
2609 let mut head = Self {
2610 version: STORE_PROTOCOL_VERSION,
2611 store_root_hash,
2612 author_registration,
2613 commit,
2614 successor,
2615 signature: String::new(),
2616 };
2617 let (_, signature) = keys::sign_hex(signer, &head.canonical_signed_bytes());
2618 head.signature = signature;
2619 Ok(head)
2620 }
2621
2622 pub(crate) fn canonical_signed_bytes(&self) -> Vec<u8> {
2623 domain_json(
2624 HEAD_DOMAIN,
2625 &HeadSignedFields {
2626 version: self.version,
2627 store_root_hash: self.store_root_hash,
2628 author_registration: &self.author_registration,
2629 commit: &self.commit,
2630 successor: &self.successor,
2631 },
2632 )
2633 }
2634
2635 pub fn to_bytes(&self) -> Vec<u8> {
2636 serde_json::to_vec(self).expect("StoreDeviceHead serialization cannot fail")
2637 }
2638
2639 pub fn head_hash(&self) -> ObjectHash {
2640 ObjectHash::digest(&self.canonical_signed_bytes())
2641 }
2642
2643 pub fn slot_sequence(&self) -> u64 {
2644 self.commit.coord.sequence()
2645 }
2646
2647 pub fn parse_at(
2648 bytes: &[u8],
2649 expected_store_root_hash: ObjectHash,
2650 expected_registration: &StoreDeviceRegistration,
2651 expected_ref: &StoreBatchCommitRef,
2652 ) -> Result<Self, StoreProtocolError> {
2653 let head: Self = serde_json::from_slice(bytes)
2654 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
2655 require_version(head.version)?;
2656 if head.store_root_hash != expected_store_root_hash {
2657 return Err(StoreProtocolError::StoreRootMismatch {
2658 expected: expected_store_root_hash,
2659 actual: head.store_root_hash,
2660 });
2661 }
2662 head.author_registration
2663 .verify_registration(expected_registration)?;
2664 if &head.commit != expected_ref {
2665 return Err(StoreProtocolError::Malformed(
2666 "Store head activates a different exact commit".to_string(),
2667 ));
2668 }
2669 if head.commit.coord.sequence() == 0 {
2670 return Err(StoreProtocolError::InvalidSequence(0));
2671 }
2672 if !keys::verify_signature_hex(
2673 &expected_registration.device_signing_pubkey,
2674 &head.signature,
2675 &head.canonical_signed_bytes(),
2676 ) {
2677 return Err(StoreProtocolError::InvalidSignature);
2678 }
2679 Ok(head)
2680 }
2681}
2682
2683#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2684#[serde(deny_unknown_fields)]
2685pub struct StoreDeviceHeadRef {
2686 pub head_hash: ObjectHash,
2687 pub object: ExactObjectRef,
2688}
2689
2690#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2692#[serde(deny_unknown_fields)]
2693pub struct StoreSerialHead {
2694 pub version: u32,
2695 pub store_root_hash: ObjectHash,
2696 pub state: StoreSerialHeadState,
2697 pub signature: String,
2698}
2699
2700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2701#[serde(rename_all = "snake_case", deny_unknown_fields)]
2702pub enum StoreSerialHeadState {
2703 Genesis {
2704 root: StoreRootRef,
2705 founder_registration: StoreDeviceRegistrationRef,
2706 },
2707 Commit {
2708 author_registration: StoreDeviceRegistrationRef,
2709 commit: StoreBatchCommitRef,
2710 },
2711}
2712
2713#[derive(Serialize)]
2714struct SerialHeadSignedFields<'a> {
2715 version: u32,
2716 store_root_hash: ObjectHash,
2717 state: &'a StoreSerialHeadState,
2718}
2719
2720impl StoreSerialHead {
2721 pub fn signed(
2722 store_root_hash: ObjectHash,
2723 state: StoreSerialHeadState,
2724 signer: &UserKeypair,
2725 ) -> Result<Self, StoreProtocolError> {
2726 validate_serial_head_state(&state)?;
2727 let mut head = Self {
2728 version: STORE_PROTOCOL_VERSION,
2729 store_root_hash,
2730 state,
2731 signature: String::new(),
2732 };
2733 let (_, signature) = keys::sign_hex(signer, &head.canonical_signed_bytes());
2734 head.signature = signature;
2735 Ok(head)
2736 }
2737
2738 fn canonical_signed_bytes(&self) -> Vec<u8> {
2739 domain_json(
2740 SERIAL_HEAD_DOMAIN,
2741 &SerialHeadSignedFields {
2742 version: self.version,
2743 store_root_hash: self.store_root_hash,
2744 state: &self.state,
2745 },
2746 )
2747 }
2748
2749 pub fn to_bytes(&self) -> Vec<u8> {
2750 serde_json::to_vec(self).expect("StoreSerialHead serialization cannot fail")
2751 }
2752
2753 pub fn head_hash(&self) -> ObjectHash {
2754 ObjectHash::digest(&self.canonical_signed_bytes())
2755 }
2756
2757 pub fn parse(
2758 bytes: &[u8],
2759 expected_store_root_hash: ObjectHash,
2760 executor: &StoreDeviceRegistration,
2761 ) -> Result<Self, StoreProtocolError> {
2762 let head: Self = serde_json::from_slice(bytes)
2763 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
2764 require_version(head.version)?;
2765 if head.store_root_hash != expected_store_root_hash {
2766 return Err(StoreProtocolError::StoreRootMismatch {
2767 expected: expected_store_root_hash,
2768 actual: head.store_root_hash,
2769 });
2770 }
2771 validate_serial_head_state(&head.state)?;
2772 match &head.state {
2773 StoreSerialHeadState::Genesis {
2774 founder_registration,
2775 ..
2776 } => founder_registration.verify_registration(executor)?,
2777 StoreSerialHeadState::Commit {
2778 author_registration,
2779 ..
2780 } => author_registration.verify_registration(executor)?,
2781 }
2782 if !keys::verify_signature_hex(
2783 &executor.device_signing_pubkey,
2784 &head.signature,
2785 &head.canonical_signed_bytes(),
2786 ) {
2787 return Err(StoreProtocolError::InvalidSignature);
2788 }
2789 Ok(head)
2790 }
2791}
2792
2793fn validate_serial_head_state(state: &StoreSerialHeadState) -> Result<(), StoreProtocolError> {
2794 match state {
2795 StoreSerialHeadState::Genesis { .. } => Ok(()),
2796 StoreSerialHeadState::Commit { commit, .. } => match commit.coord {
2797 StoreCommitCoord::Serial { sequence } if sequence > 0 => Ok(()),
2798 _ => Err(StoreProtocolError::InvalidSerialHead),
2799 },
2800 }
2801}
2802
2803const STORE_DEVICE_ID_DOMAIN: &[u8] = b"coven.store-device-id.v1\0";
2804
2805#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2806#[serde(transparent)]
2807pub struct StoreDeviceId(ObjectHash);
2808
2809impl StoreDeviceId {
2810 pub fn derive(store_root: &StoreRootRef, origin: &StoreDeviceRegistrationOrigin) -> Self {
2811 let mut material = STORE_DEVICE_ID_DOMAIN.to_vec();
2812 material.extend(
2813 serde_json::to_vec(&(store_root, origin.external_id()))
2814 .expect("Store device identity serialization cannot fail"),
2815 );
2816 Self(ObjectHash::digest(&material))
2817 }
2818}
2819
2820impl fmt::Display for StoreDeviceId {
2821 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2822 fmt::Display::fmt(&self.0, formatter)
2823 }
2824}
2825
2826impl FromStr for StoreDeviceId {
2827 type Err = StoreProtocolError;
2828
2829 fn from_str(value: &str) -> Result<Self, Self::Err> {
2830 value.parse().map(Self)
2831 }
2832}
2833
2834#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2835#[serde(transparent)]
2836pub struct StoreCreationId(ObjectHash);
2837
2838impl StoreCreationId {
2839 pub fn from_random_bytes(bytes: [u8; 32]) -> Self {
2840 Self(ObjectHash::from_digest(bytes))
2841 }
2842
2843 #[cfg(test)]
2844 pub(crate) fn from_nonce(nonce: &str) -> Self {
2845 Self(ObjectHash::digest(nonce.as_bytes()))
2846 }
2847}
2848
2849impl fmt::Display for StoreCreationId {
2850 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2851 fmt::Display::fmt(&self.0, formatter)
2852 }
2853}
2854
2855#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2856#[serde(transparent)]
2857pub struct DeviceJoinAttemptId(ObjectHash);
2858
2859impl DeviceJoinAttemptId {
2860 pub fn from_hash(hash: ObjectHash) -> Self {
2861 Self(hash)
2862 }
2863}
2864
2865#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2866#[serde(transparent)]
2867pub struct DeviceRecoveryId(ObjectHash);
2868
2869impl DeviceRecoveryId {
2870 pub fn from_hash(hash: ObjectHash) -> Self {
2871 Self(hash)
2872 }
2873}
2874
2875#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2876#[serde(deny_unknown_fields)]
2877pub struct DeviceJoinAttemptRef {
2878 pub attempt_id: DeviceJoinAttemptId,
2879 pub attempt_hash: ObjectHash,
2880 pub object: ExactObjectRef,
2881}
2882
2883#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2884#[serde(deny_unknown_fields)]
2885pub struct DeviceJoinAttempt {
2886 pub version: u32,
2887 pub store_root: StoreRootRef,
2888 pub attempt_id: DeviceJoinAttemptId,
2889 pub attempt_slot: ObjectSlot,
2890 pub expected_registration: StoreDeviceRegistration,
2891 pub registration_slot: ObjectSlot,
2892 pub outcome_slot: ObjectSlot,
2893 pub bootstrap_cut: StoreHistoryCut,
2894 pub membership: StoreMembershipStateRef,
2895 pub provider_admin_grant: super::provider::ProviderAdminGrantId,
2896 pub provider_approval: super::device_join::DeviceProviderAdmissionApproval,
2897 pub provider_response: super::device_join::DeviceProviderResponseReservation,
2898 pub owner_registration: StoreDeviceRegistrationRef,
2899 pub owner_grant: MembershipGrantId,
2900 pub signature: String,
2901}
2902
2903#[derive(Serialize)]
2904struct DeviceJoinAttemptSignedFields<'a> {
2905 version: u32,
2906 store_root: &'a StoreRootRef,
2907 attempt_id: DeviceJoinAttemptId,
2908 attempt_slot: &'a ObjectSlot,
2909 expected_registration: &'a StoreDeviceRegistration,
2910 registration_slot: &'a ObjectSlot,
2911 outcome_slot: &'a ObjectSlot,
2912 bootstrap_cut: &'a StoreHistoryCut,
2913 membership: &'a StoreMembershipStateRef,
2914 provider_admin_grant: &'a super::provider::ProviderAdminGrantId,
2915 provider_approval: &'a super::device_join::DeviceProviderAdmissionApproval,
2916 provider_response: &'a super::device_join::DeviceProviderResponseReservation,
2917 owner_registration: &'a StoreDeviceRegistrationRef,
2918 owner_grant: &'a MembershipGrantId,
2919}
2920
2921impl DeviceJoinAttempt {
2922 #[allow(clippy::too_many_arguments)]
2923 pub fn signed(
2924 store_root: StoreRootRef,
2925 attempt_id: DeviceJoinAttemptId,
2926 attempt_slot: ObjectSlot,
2927 expected_registration: StoreDeviceRegistration,
2928 registration_slot: ObjectSlot,
2929 outcome_slot: ObjectSlot,
2930 bootstrap_cut: StoreHistoryCut,
2931 membership: StoreMembershipStateRef,
2932 provider_admin_grant: super::provider::ProviderAdminGrantId,
2933 provider_approval: super::device_join::DeviceProviderAdmissionApproval,
2934 provider_response: super::device_join::DeviceProviderResponseReservation,
2935 owner_registration: StoreDeviceRegistrationRef,
2936 owner_grant: MembershipGrantId,
2937 owner: &StoreDeviceRegistration,
2938 owner_device_signer: &UserKeypair,
2939 ) -> Result<Self, StoreProtocolError> {
2940 owner_registration.verify_registration(owner)?;
2941 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
2942 return Err(StoreProtocolError::InvalidSignature);
2943 }
2944 let mut attempt = Self {
2945 version: STORE_PROTOCOL_VERSION,
2946 store_root,
2947 attempt_id,
2948 attempt_slot,
2949 expected_registration,
2950 registration_slot,
2951 outcome_slot,
2952 bootstrap_cut,
2953 membership,
2954 provider_admin_grant,
2955 provider_approval,
2956 provider_response,
2957 owner_registration,
2958 owner_grant,
2959 signature: String::new(),
2960 };
2961 attempt.validate_shape()?;
2962 let (_, signature) = keys::sign_hex(owner_device_signer, &attempt.canonical_signed_bytes());
2963 attempt.signature = signature;
2964 Ok(attempt)
2965 }
2966
2967 fn canonical_signed_bytes(&self) -> Vec<u8> {
2968 domain_json(
2969 DEVICE_JOIN_ATTEMPT_DOMAIN,
2970 &DeviceJoinAttemptSignedFields {
2971 version: self.version,
2972 store_root: &self.store_root,
2973 attempt_id: self.attempt_id,
2974 attempt_slot: &self.attempt_slot,
2975 expected_registration: &self.expected_registration,
2976 registration_slot: &self.registration_slot,
2977 outcome_slot: &self.outcome_slot,
2978 bootstrap_cut: &self.bootstrap_cut,
2979 membership: &self.membership,
2980 provider_admin_grant: &self.provider_admin_grant,
2981 provider_approval: &self.provider_approval,
2982 provider_response: &self.provider_response,
2983 owner_registration: &self.owner_registration,
2984 owner_grant: &self.owner_grant,
2985 },
2986 )
2987 }
2988
2989 pub fn attempt_hash(&self) -> ObjectHash {
2990 ObjectHash::digest(&self.canonical_signed_bytes())
2991 }
2992
2993 pub fn to_bytes(&self) -> Vec<u8> {
2994 serde_json::to_vec(self).expect("DeviceJoinAttempt serialization cannot fail")
2995 }
2996
2997 pub fn parse_at(
2998 bytes: &[u8],
2999 expected: &DeviceJoinAttemptRef,
3000 owner: &StoreDeviceRegistration,
3001 ) -> Result<Self, StoreProtocolError> {
3002 let attempt: Self = serde_json::from_slice(bytes)
3003 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
3004 require_version(attempt.version)?;
3005 attempt.validate_shape()?;
3006 if attempt.attempt_id != expected.attempt_id
3007 || attempt.attempt_hash() != expected.attempt_hash
3008 || &attempt.attempt_slot != expected.object.slot()
3009 {
3010 return Err(StoreProtocolError::JoinAttemptMismatch);
3011 }
3012 attempt.owner_registration.verify_registration(owner)?;
3013 if !keys::verify_signature_hex(
3014 &owner.device_signing_pubkey,
3015 &attempt.signature,
3016 &attempt.canonical_signed_bytes(),
3017 ) {
3018 return Err(StoreProtocolError::InvalidSignature);
3019 }
3020 Ok(attempt)
3021 }
3022
3023 fn validate_shape(&self) -> Result<(), StoreProtocolError> {
3024 let registration_policy = match &self.expected_registration.store_commits {
3025 StoreCommitAnchor::MergeConcurrent { .. } => WritePolicy::MergeConcurrent,
3026 StoreCommitAnchor::Serial => WritePolicy::Serial,
3027 };
3028 validate_store_history_cut(&self.bootstrap_cut)?;
3029 if !matches!(
3030 (registration_policy, &self.bootstrap_cut),
3031 (
3032 WritePolicy::MergeConcurrent,
3033 StoreHistoryCut::MergeConcurrent(_)
3034 ) | (WritePolicy::Serial, StoreHistoryCut::Serial(_))
3035 ) {
3036 return Err(StoreProtocolError::JoinAttemptMismatch);
3037 }
3038 if self.expected_registration.store_root != self.store_root
3039 || self.expected_registration.device_id
3040 != StoreDeviceId::derive(&self.store_root, &self.expected_registration.origin)
3041 || self.attempt_slot == self.registration_slot
3042 || self.attempt_slot == self.outcome_slot
3043 || self.registration_slot == self.outcome_slot
3044 || self.membership.write_policy() != registration_policy
3045 || self.provider_admin_grant
3046 != self.provider_approval.request.offer.provider_admin.grant_id
3047 || self.provider_approval.request.offer.store_root != self.store_root
3048 || self.provider_approval.request.offer.attempt_id != self.attempt_id
3049 || self.provider_approval.request.offer.attempt_slot != self.attempt_slot
3050 || self.provider_approval.request.offer.outcome_slot != self.outcome_slot
3051 || self.provider_approval.request.offer.owner_registration != self.owner_registration
3052 || self.provider_approval.request.offer.owner_grant != self.owner_grant
3053 || self.provider_approval.request.offer.member_pubkey
3054 != self.expected_registration.author_pubkey
3055 || self.provider_approval.request.peer_provider != self.expected_registration.provider
3056 {
3057 return Err(StoreProtocolError::JoinAttemptMismatch);
3058 }
3059 match (&self.provider_approval.admission, &self.provider_response) {
3060 (
3061 super::device_join::DeviceProviderAdmissionChallenge::SamePrincipal,
3062 super::device_join::DeviceProviderResponseReservation::SamePrincipal,
3063 )
3064 | (
3065 super::device_join::DeviceProviderAdmissionChallenge::CrossPrincipal(_),
3066 super::device_join::DeviceProviderResponseReservation::CrossPrincipal { .. },
3067 ) => {}
3068 _ => return Err(StoreProtocolError::JoinAttemptMismatch),
3069 }
3070 match &self.expected_registration.origin {
3071 StoreDeviceRegistrationOrigin::Join {
3072 attempt_id,
3073 attempt_slot,
3074 outcome_slot,
3075 } if *attempt_id == self.attempt_id
3076 && attempt_slot == &self.attempt_slot
3077 && outcome_slot == &self.outcome_slot =>
3078 {
3079 Ok(())
3080 }
3081 _ => Err(StoreProtocolError::JoinAttemptMismatch),
3082 }
3083 }
3084}
3085
3086#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3087#[serde(deny_unknown_fields)]
3088pub struct DeviceReadinessProof {
3089 pub version: u32,
3090 pub store_root_hash: ObjectHash,
3091 pub attempt: DeviceJoinAttemptRef,
3092 pub registration: StoreDeviceRegistrationRef,
3093 pub initial_ack: StoreAckRef,
3094 pub bootstrap_cut: StoreHistoryCut,
3095 pub signature: String,
3096}
3097
3098#[derive(Serialize)]
3099struct DeviceReadinessSignedFields<'a> {
3100 version: u32,
3101 store_root_hash: ObjectHash,
3102 attempt: &'a DeviceJoinAttemptRef,
3103 registration: &'a StoreDeviceRegistrationRef,
3104 initial_ack: &'a StoreAckRef,
3105 bootstrap_cut: &'a StoreHistoryCut,
3106}
3107
3108impl DeviceReadinessProof {
3109 pub fn signed(
3110 attempt: DeviceJoinAttemptRef,
3111 registration: StoreDeviceRegistrationRef,
3112 initial_ack: StoreAckRef,
3113 bootstrap_cut: StoreHistoryCut,
3114 registration_value: &StoreDeviceRegistration,
3115 device_signer: &UserKeypair,
3116 ) -> Result<Self, StoreProtocolError> {
3117 registration.verify_registration(registration_value)?;
3118 if keys::public_key_hex(device_signer) != registration_value.device_signing_pubkey {
3119 return Err(StoreProtocolError::InvalidSignature);
3120 }
3121 let mut proof = Self {
3122 version: STORE_PROTOCOL_VERSION,
3123 store_root_hash: registration_value.store_root.store_root_hash,
3124 attempt,
3125 registration,
3126 initial_ack,
3127 bootstrap_cut,
3128 signature: String::new(),
3129 };
3130 validate_store_history_cut(&proof.bootstrap_cut)?;
3131 let (_, signature) = keys::sign_hex(device_signer, &proof.canonical_signed_bytes());
3132 proof.signature = signature;
3133 Ok(proof)
3134 }
3135
3136 fn canonical_signed_bytes(&self) -> Vec<u8> {
3137 domain_json(
3138 DEVICE_READINESS_DOMAIN,
3139 &DeviceReadinessSignedFields {
3140 version: self.version,
3141 store_root_hash: self.store_root_hash,
3142 attempt: &self.attempt,
3143 registration: &self.registration,
3144 initial_ack: &self.initial_ack,
3145 bootstrap_cut: &self.bootstrap_cut,
3146 },
3147 )
3148 }
3149
3150 pub fn verify(
3151 &self,
3152 attempt_ref: &DeviceJoinAttemptRef,
3153 attempt: &DeviceJoinAttempt,
3154 registration: &StoreDeviceRegistration,
3155 initial_ack_ref: &StoreAckRef,
3156 initial_ack: &StoreAck,
3157 ) -> Result<(), StoreProtocolError> {
3158 require_version(self.version)?;
3159 if &self.attempt != attempt_ref
3160 || attempt_ref.attempt_id != attempt.attempt_id
3161 || attempt_ref.attempt_hash != attempt.attempt_hash()
3162 || self.store_root_hash != registration.store_root.store_root_hash
3163 || self.registration.device_id != registration.device_id
3164 || self.bootstrap_cut != attempt.bootstrap_cut
3165 {
3166 return Err(StoreProtocolError::DeviceReadinessMismatch);
3167 }
3168 self.registration.verify_registration(registration)?;
3169 if initial_ack.author_registration != self.registration
3170 || initial_ack.revision != 1
3171 || initial_ack.predecessor.is_some()
3172 || initial_ack_ref != &self.initial_ack
3173 || initial_ack_ref.revision != initial_ack.revision
3174 || initial_ack_ref.ack_hash != initial_ack.ack_hash()
3175 || initial_ack.store_cut != self.bootstrap_cut
3176 {
3177 return Err(StoreProtocolError::DeviceReadinessMismatch);
3178 }
3179 validate_store_history_cut(&self.bootstrap_cut)?;
3180 if !keys::verify_signature_hex(
3181 ®istration.device_signing_pubkey,
3182 &self.signature,
3183 &self.canonical_signed_bytes(),
3184 ) {
3185 return Err(StoreProtocolError::InvalidSignature);
3186 }
3187 Ok(())
3188 }
3189}
3190
3191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3192#[serde(rename_all = "snake_case", deny_unknown_fields)]
3193pub enum DeviceJoinOutcomeBody {
3194 Activated { readiness: DeviceReadinessProof },
3195 Cancelled,
3196}
3197
3198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3199#[serde(deny_unknown_fields)]
3200pub struct DeviceJoinOutcome {
3201 pub version: u32,
3202 pub store_root_hash: ObjectHash,
3203 pub attempt: DeviceJoinAttemptRef,
3204 pub body: DeviceJoinOutcomeBody,
3205 pub owner_registration: StoreDeviceRegistrationRef,
3206 pub owner_grant: MembershipGrantId,
3207 pub signature: String,
3208}
3209
3210#[derive(Serialize)]
3211struct DeviceJoinOutcomeSignedFields<'a> {
3212 version: u32,
3213 store_root_hash: ObjectHash,
3214 attempt: &'a DeviceJoinAttemptRef,
3215 body: &'a DeviceJoinOutcomeBody,
3216 owner_registration: &'a StoreDeviceRegistrationRef,
3217 owner_grant: &'a MembershipGrantId,
3218}
3219
3220impl DeviceJoinOutcome {
3221 pub fn signed(
3222 attempt: DeviceJoinAttemptRef,
3223 body: DeviceJoinOutcomeBody,
3224 owner_registration: StoreDeviceRegistrationRef,
3225 owner_grant: MembershipGrantId,
3226 owner: &StoreDeviceRegistration,
3227 owner_device_signer: &UserKeypair,
3228 ) -> Result<Self, StoreProtocolError> {
3229 owner_registration.verify_registration(owner)?;
3230 if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
3231 return Err(StoreProtocolError::InvalidSignature);
3232 }
3233 let mut outcome = Self {
3234 version: STORE_PROTOCOL_VERSION,
3235 store_root_hash: owner.store_root.store_root_hash,
3236 attempt,
3237 body,
3238 owner_registration,
3239 owner_grant,
3240 signature: String::new(),
3241 };
3242 let (_, signature) = keys::sign_hex(owner_device_signer, &outcome.canonical_signed_bytes());
3243 outcome.signature = signature;
3244 Ok(outcome)
3245 }
3246
3247 pub(crate) fn canonical_signed_bytes(&self) -> Vec<u8> {
3248 domain_json(
3249 DEVICE_JOIN_OUTCOME_DOMAIN,
3250 &DeviceJoinOutcomeSignedFields {
3251 version: self.version,
3252 store_root_hash: self.store_root_hash,
3253 attempt: &self.attempt,
3254 body: &self.body,
3255 owner_registration: &self.owner_registration,
3256 owner_grant: &self.owner_grant,
3257 },
3258 )
3259 }
3260
3261 pub fn outcome_hash(&self) -> ObjectHash {
3262 ObjectHash::digest(&self.canonical_signed_bytes())
3263 }
3264
3265 pub fn to_bytes(&self) -> Vec<u8> {
3266 serde_json::to_vec(self).expect("DeviceJoinOutcome serialization cannot fail")
3267 }
3268}
3269
3270#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3271#[serde(rename_all = "snake_case", deny_unknown_fields)]
3272pub enum DeviceJoinOutcomeRef {
3273 Activated {
3274 attempt: DeviceJoinAttemptRef,
3275 outcome_hash: ObjectHash,
3276 object: ExactObjectRef,
3277 },
3278 Cancelled {
3279 attempt: DeviceJoinAttemptRef,
3280 outcome_hash: ObjectHash,
3281 object: ExactObjectRef,
3282 },
3283}
3284
3285impl DeviceJoinOutcomeRef {
3286 pub fn slot(&self) -> &ObjectSlot {
3287 self.object().slot()
3288 }
3289
3290 pub fn object(&self) -> &ExactObjectRef {
3291 match self {
3292 Self::Activated { object, .. } | Self::Cancelled { object, .. } => object,
3293 }
3294 }
3295
3296 pub fn attempt(&self) -> &DeviceJoinAttemptRef {
3297 match self {
3298 Self::Activated { attempt, .. } | Self::Cancelled { attempt, .. } => attempt,
3299 }
3300 }
3301
3302 pub fn verify_outcome(&self, outcome: &DeviceJoinOutcome) -> Result<(), StoreProtocolError> {
3303 let (attempt, expected_hash, expects_activated) = match self {
3304 Self::Activated {
3305 attempt,
3306 outcome_hash,
3307 ..
3308 } => (attempt, outcome_hash, true),
3309 Self::Cancelled {
3310 attempt,
3311 outcome_hash,
3312 ..
3313 } => (attempt, outcome_hash, false),
3314 };
3315 if &outcome.attempt != attempt || outcome.outcome_hash() != *expected_hash {
3316 return Err(StoreProtocolError::JoinOutcomeMismatch);
3317 }
3318 if expects_activated != matches!(outcome.body, DeviceJoinOutcomeBody::Activated { .. }) {
3319 return Err(StoreProtocolError::JoinOutcomeMismatch);
3320 }
3321 Ok(())
3322 }
3323}
3324
3325#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3326#[serde(deny_unknown_fields)]
3327pub struct OwnerRecoveryNodeRef {
3328 pub owner_pubkey: String,
3329 pub owner_grant: MembershipGrantId,
3330 pub sequence: u64,
3331 pub node_hash: ObjectHash,
3332 pub object: ExactObjectRef,
3333}
3334
3335#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3336#[serde(transparent)]
3337pub struct OwnerRecoveryActivationId(ObjectHash);
3338
3339impl OwnerRecoveryActivationId {
3340 pub fn derive(
3341 root: &StoreRootRef,
3342 owner_pubkey: &str,
3343 owner_grant: &MembershipGrantId,
3344 anchor: &GrantStreamAnchor,
3345 ) -> Result<Self, StoreProtocolError> {
3346 if !matches!(anchor, GrantStreamAnchor::OwnerRecovery { .. }) {
3347 return Err(StoreProtocolError::OwnerRecoveryMismatch);
3348 }
3349 Ok(Self(ObjectHash::digest(&domain_json(
3350 b"coven.owner-recovery-activation.v1\0",
3351 &(root, owner_pubkey, owner_grant, anchor),
3352 ))))
3353 }
3354}
3355
3356#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3357#[serde(rename_all = "snake_case", deny_unknown_fields)]
3358pub enum OwnerRecoveryPosition {
3359 BeforeFirst {
3360 activation: OwnerRecoveryActivationId,
3361 },
3362 At {
3363 node: OwnerRecoveryNodeRef,
3364 },
3365}
3366
3367#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3368#[serde(deny_unknown_fields)]
3369pub struct OwnerRecoveryCursor {
3370 pub owner_grant: MembershipGrantId,
3371 pub position: OwnerRecoveryPosition,
3372}
3373
3374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3375#[serde(rename_all = "snake_case", deny_unknown_fields)]
3376pub enum StoreDeviceStateRef {
3377 MergeConcurrent {
3378 frontier: CommitFrontier,
3379 recovery: Vec<OwnerRecoveryCursor>,
3380 state_hash: ObjectHash,
3381 },
3382 Serial {
3383 position: SerialStorePosition,
3384 recovery: Vec<OwnerRecoveryCursor>,
3385 state_hash: ObjectHash,
3386 },
3387}
3388
3389impl StoreDeviceStateRef {
3390 pub fn merge_concurrent(
3391 frontier: CommitFrontier,
3392 state: &ResolvedStoreDeviceState,
3393 ) -> Result<Self, StoreProtocolError> {
3394 validate_commit_frontier(&frontier)?;
3395 if !matches!(frontier, CommitFrontier::MergeConcurrent(_)) {
3396 return Err(StoreProtocolError::WritePolicyMismatch {
3397 expected: WritePolicy::MergeConcurrent,
3398 actual: WritePolicy::Serial,
3399 });
3400 }
3401 validate_recovery_cursors(&state.recovery)?;
3402 Ok(Self::MergeConcurrent {
3403 frontier,
3404 recovery: state.recovery.clone(),
3405 state_hash: state.state_hash,
3406 })
3407 }
3408
3409 pub fn serial(
3410 position: SerialStorePosition,
3411 state: &ResolvedStoreDeviceState,
3412 ) -> Result<Self, StoreProtocolError> {
3413 validate_recovery_cursors(&state.recovery)?;
3414 Ok(Self::Serial {
3415 position,
3416 recovery: state.recovery.clone(),
3417 state_hash: state.state_hash,
3418 })
3419 }
3420
3421 pub fn state_hash(&self) -> ObjectHash {
3422 match self {
3423 Self::MergeConcurrent { state_hash, .. } | Self::Serial { state_hash, .. } => {
3424 *state_hash
3425 }
3426 }
3427 }
3428
3429 pub fn recovery(&self) -> &[OwnerRecoveryCursor] {
3430 match self {
3431 Self::MergeConcurrent { recovery, .. } | Self::Serial { recovery, .. } => recovery,
3432 }
3433 }
3434
3435 pub fn write_policy(&self) -> WritePolicy {
3436 match self {
3437 Self::MergeConcurrent { .. } => WritePolicy::MergeConcurrent,
3438 Self::Serial { .. } => WritePolicy::Serial,
3439 }
3440 }
3441}
3442
3443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3444#[serde(rename_all = "snake_case", deny_unknown_fields)]
3445pub enum StoreDeviceStatus {
3446 Active,
3447 Inactive {
3448 terminals: Vec<StoreDeviceTerminalRef>,
3449 accepted_cut: StoreHistoryCut,
3450 },
3451}
3452
3453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3454#[serde(deny_unknown_fields)]
3455pub struct StoreDeviceRecord {
3456 pub registration: StoreDeviceRegistrationRef,
3457 pub proposals: BTreeMap<StoreDeviceExclusionProposalId, StoreDeviceProposalState>,
3458 pub status: StoreDeviceStatus,
3459}
3460
3461#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3462#[serde(transparent)]
3463pub struct StoreDeviceExclusionProposalId(ObjectHash);
3464
3465#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3466#[serde(deny_unknown_fields)]
3467pub struct StoreDeviceExclusionProposalRef {
3468 pub proposal_id: StoreDeviceExclusionProposalId,
3469 pub proposal_hash: ObjectHash,
3470 pub object: ExactObjectRef,
3471}
3472
3473#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3474#[serde(deny_unknown_fields)]
3475pub struct StoreDeviceExclusionRef {
3476 pub proposal: StoreDeviceExclusionProposalRef,
3477 pub outcome_hash: ObjectHash,
3478 pub object: ExactObjectRef,
3479}
3480
3481#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3482#[serde(deny_unknown_fields)]
3483pub struct StoreDeviceExclusionCancellationRef {
3484 pub proposal: StoreDeviceExclusionProposalRef,
3485 pub outcome_hash: ObjectHash,
3486 pub object: ExactObjectRef,
3487}
3488
3489#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3490#[serde(deny_unknown_fields)]
3491pub struct StoreDeviceSelfRetirementRef {
3492 pub candidate_family: CandidateFamilyId,
3493 pub target: StoreDeviceRegistrationRef,
3494 pub retiring_cut: StoreHistoryCut,
3495 pub retirement_hash: ObjectHash,
3496 pub object: ExactObjectRef,
3497}
3498
3499#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3500#[serde(deny_unknown_fields)]
3501pub struct StoreDeviceSelfRetirement {
3502 pub version: u32,
3503 pub store_root_hash: ObjectHash,
3504 pub candidate_family: CandidateFamilyId,
3505 pub target: StoreDeviceRegistrationRef,
3506 pub retiring_cut: StoreHistoryCut,
3507 pub signature: String,
3508}
3509
3510#[derive(Serialize)]
3511struct StoreDeviceSelfRetirementSignedFields<'a> {
3512 version: u32,
3513 store_root_hash: ObjectHash,
3514 candidate_family: CandidateFamilyId,
3515 target: &'a StoreDeviceRegistrationRef,
3516 retiring_cut: &'a StoreHistoryCut,
3517}
3518
3519impl StoreDeviceSelfRetirement {
3520 pub fn signed(
3521 store_root_hash: ObjectHash,
3522 candidate_family: CandidateFamilyId,
3523 target: StoreDeviceRegistrationRef,
3524 retiring_cut: StoreHistoryCut,
3525 device_signer: &UserKeypair,
3526 ) -> Result<Self, StoreProtocolError> {
3527 validate_store_history_cut(&retiring_cut)?;
3528 let mut retirement = Self {
3529 version: STORE_PROTOCOL_VERSION,
3530 store_root_hash,
3531 candidate_family,
3532 target,
3533 retiring_cut,
3534 signature: String::new(),
3535 };
3536 let (_, signature) = keys::sign_hex(device_signer, &retirement.canonical_signed_bytes());
3537 retirement.signature = signature;
3538 Ok(retirement)
3539 }
3540
3541 fn canonical_signed_bytes(&self) -> Vec<u8> {
3542 domain_json(
3543 SELF_RETIREMENT_DOMAIN,
3544 &StoreDeviceSelfRetirementSignedFields {
3545 version: self.version,
3546 store_root_hash: self.store_root_hash,
3547 candidate_family: self.candidate_family,
3548 target: &self.target,
3549 retiring_cut: &self.retiring_cut,
3550 },
3551 )
3552 }
3553
3554 pub fn retirement_hash(&self) -> ObjectHash {
3555 ObjectHash::digest(&self.canonical_signed_bytes())
3556 }
3557
3558 pub fn to_bytes(&self) -> Vec<u8> {
3559 serde_json::to_vec(self).expect("StoreDeviceSelfRetirement serialization cannot fail")
3560 }
3561
3562 pub fn parse_at(
3563 bytes: &[u8],
3564 expected: &StoreDeviceSelfRetirementRef,
3565 registration: &StoreDeviceRegistration,
3566 ) -> Result<Self, StoreProtocolError> {
3567 let retirement: Self = serde_json::from_slice(bytes)
3568 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
3569 require_version(retirement.version)?;
3570 validate_store_history_cut(&retirement.retiring_cut)?;
3571 if retirement.store_root_hash != registration.store_root.store_root_hash
3572 || retirement.candidate_family != expected.candidate_family
3573 || retirement.target != expected.target
3574 || retirement.retiring_cut != expected.retiring_cut
3575 || retirement.retirement_hash() != expected.retirement_hash
3576 {
3577 return Err(StoreProtocolError::DeviceStateMismatch);
3578 }
3579 let prefix = device_self_retirement_semantic_prefix(
3580 expected.candidate_family,
3581 &expected.target.device_id,
3582 expected.retirement_hash,
3583 );
3584 if expected.object.slot().logical_key() != format!("{prefix}.json") {
3585 return Err(StoreProtocolError::RelocatedSlot {
3586 expected: prefix,
3587 actual: expected.object.slot().logical_key().to_string(),
3588 });
3589 }
3590 expected.target.verify_registration(registration)?;
3591 if !keys::verify_signature_hex(
3592 ®istration.device_signing_pubkey,
3593 &retirement.signature,
3594 &retirement.canonical_signed_bytes(),
3595 ) {
3596 return Err(StoreProtocolError::InvalidSignature);
3597 }
3598 Ok(retirement)
3599 }
3600}
3601
3602impl StoreDeviceSelfRetirementRef {
3603 pub fn from_retirement(retirement: &StoreDeviceSelfRetirement, object: ExactObjectRef) -> Self {
3604 Self {
3605 candidate_family: retirement.candidate_family,
3606 target: retirement.target.clone(),
3607 retiring_cut: retirement.retiring_cut.clone(),
3608 retirement_hash: retirement.retirement_hash(),
3609 object,
3610 }
3611 }
3612}
3613
3614#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3615#[serde(rename_all = "snake_case", deny_unknown_fields)]
3616pub enum StoreDeviceTerminalRef {
3617 Excluded(StoreDeviceExclusionRef),
3618 SelfRetirement(StoreDeviceSelfRetirementRef),
3619}
3620
3621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3622#[serde(rename_all = "snake_case", deny_unknown_fields)]
3623pub enum StoreDeviceProposalState {
3624 Pending {
3625 proposal: StoreDeviceExclusionProposalRef,
3626 },
3627 Cancelled {
3628 outcome: StoreDeviceExclusionCancellationRef,
3629 },
3630 Superseded {
3631 proposal: StoreDeviceExclusionProposalRef,
3632 terminals: Vec<StoreDeviceTerminalRef>,
3633 },
3634}
3635
3636#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3637#[serde(deny_unknown_fields)]
3638pub struct ResolvedStoreDeviceState {
3639 pub devices: BTreeMap<StoreDeviceId, StoreDeviceRecord>,
3640 pub recovery: Vec<OwnerRecoveryCursor>,
3641 pub state_hash: ObjectHash,
3642}
3643
3644impl ResolvedStoreDeviceState {
3645 pub fn founder(
3646 root: &StoreRootRef,
3647 founder_registration: StoreDeviceRegistrationRef,
3648 founder_pubkey: &str,
3649 founder_grant: MembershipGrantId,
3650 founder_recovery: &GrantStreamAnchor,
3651 ) -> Result<Self, StoreProtocolError> {
3652 let cursor = OwnerRecoveryCursor {
3653 owner_grant: founder_grant.clone(),
3654 position: OwnerRecoveryPosition::BeforeFirst {
3655 activation: OwnerRecoveryActivationId::derive(
3656 root,
3657 founder_pubkey,
3658 &founder_grant,
3659 founder_recovery,
3660 )?,
3661 },
3662 };
3663 let devices = BTreeMap::from([(
3664 founder_registration.device_id,
3665 StoreDeviceRecord {
3666 registration: founder_registration,
3667 proposals: BTreeMap::new(),
3668 status: StoreDeviceStatus::Active,
3669 },
3670 )]);
3671 Self::from_parts(devices, vec![cursor])
3672 }
3673
3674 pub fn activate_registration(
3675 &self,
3676 registration: StoreDeviceRegistrationRef,
3677 recovery: Option<OwnerRecoveryCursor>,
3678 ) -> Result<Self, StoreProtocolError> {
3679 if self.devices.contains_key(®istration.device_id) {
3680 return Err(StoreProtocolError::DuplicateDeviceRegistration {
3681 device_id: registration.device_id.to_string(),
3682 revision: 1,
3683 });
3684 }
3685 let mut devices = self.devices.clone();
3686 devices.insert(
3687 registration.device_id,
3688 StoreDeviceRecord {
3689 registration,
3690 proposals: BTreeMap::new(),
3691 status: StoreDeviceStatus::Active,
3692 },
3693 );
3694 let mut cursors = self.recovery.clone();
3695 if let Some(cursor) = recovery {
3696 if let Some(existing) = cursors
3697 .iter_mut()
3698 .find(|existing| existing.owner_grant == cursor.owner_grant)
3699 {
3700 *existing = cursor;
3701 } else {
3702 cursors.push(cursor);
3703 }
3704 }
3705 Self::from_parts(devices, cursors)
3706 }
3707
3708 pub fn self_retire(
3709 &self,
3710 retirement: StoreDeviceSelfRetirementRef,
3711 ) -> Result<Self, StoreProtocolError> {
3712 let mut devices = self.devices.clone();
3713 let record = devices
3714 .get_mut(&retirement.target.device_id)
3715 .ok_or(StoreProtocolError::DeviceStateMismatch)?;
3716 if record.registration != retirement.target
3717 || !matches!(record.status, StoreDeviceStatus::Active)
3718 {
3719 return Err(StoreProtocolError::DeviceStateMismatch);
3720 }
3721 record.status = StoreDeviceStatus::Inactive {
3722 terminals: vec![StoreDeviceTerminalRef::SelfRetirement(retirement.clone())],
3723 accepted_cut: retirement.retiring_cut,
3724 };
3725 Self::from_parts(devices, self.recovery.clone())
3726 }
3727
3728 pub fn merge(states: impl IntoIterator<Item = Self>) -> Result<Self, StoreProtocolError> {
3729 let mut devices = BTreeMap::new();
3730 let mut recovery = BTreeMap::<MembershipGrantId, OwnerRecoveryPosition>::new();
3731 for state in states {
3732 for (device_id, record) in state.devices {
3733 match devices.entry(device_id) {
3734 std::collections::btree_map::Entry::Vacant(entry) => {
3735 entry.insert(record);
3736 }
3737 std::collections::btree_map::Entry::Occupied(mut entry) => {
3738 if entry.get().registration != record.registration
3739 || entry.get().proposals != record.proposals
3740 {
3741 return Err(StoreProtocolError::DeviceStateMismatch);
3742 }
3743 match (&entry.get().status, &record.status) {
3744 (StoreDeviceStatus::Active, StoreDeviceStatus::Active)
3745 | (StoreDeviceStatus::Inactive { .. }, StoreDeviceStatus::Active) => {}
3746 (StoreDeviceStatus::Active, StoreDeviceStatus::Inactive { .. }) => {
3747 entry.get_mut().status = record.status;
3748 }
3749 (
3750 StoreDeviceStatus::Inactive {
3751 terminals: left,
3752 accepted_cut: left_cut,
3753 },
3754 StoreDeviceStatus::Inactive {
3755 terminals: right,
3756 accepted_cut: right_cut,
3757 },
3758 ) if left == right && left_cut == right_cut => {}
3759 (
3760 StoreDeviceStatus::Inactive { .. },
3761 StoreDeviceStatus::Inactive { .. },
3762 ) => {
3763 return Err(StoreProtocolError::DeviceStateMismatch);
3764 }
3765 }
3766 }
3767 }
3768 }
3769 for cursor in state.recovery {
3770 match recovery.entry(cursor.owner_grant) {
3771 std::collections::btree_map::Entry::Vacant(entry) => {
3772 entry.insert(cursor.position);
3773 }
3774 std::collections::btree_map::Entry::Occupied(entry) => {
3775 if entry.get() != &cursor.position {
3776 return Err(StoreProtocolError::OwnerRecoveryMismatch);
3777 }
3778 }
3779 }
3780 }
3781 }
3782 Self::from_parts(
3783 devices,
3784 recovery
3785 .into_iter()
3786 .map(|(owner_grant, position)| OwnerRecoveryCursor {
3787 owner_grant,
3788 position,
3789 })
3790 .collect(),
3791 )
3792 }
3793
3794 fn from_parts(
3795 devices: BTreeMap<StoreDeviceId, StoreDeviceRecord>,
3796 mut recovery: Vec<OwnerRecoveryCursor>,
3797 ) -> Result<Self, StoreProtocolError> {
3798 recovery.sort();
3799 validate_recovery_cursors(&recovery)?;
3800 validate_store_device_records(&devices)?;
3801 let state_hash = ObjectHash::digest(&domain_json(
3802 b"coven.store-device-state.v1\0",
3803 &(&devices, &recovery),
3804 ));
3805 Ok(Self {
3806 devices,
3807 recovery,
3808 state_hash,
3809 })
3810 }
3811}
3812
3813fn validate_store_device_records(
3814 devices: &BTreeMap<StoreDeviceId, StoreDeviceRecord>,
3815) -> Result<(), StoreProtocolError> {
3816 for (device_id, record) in devices {
3817 if record.registration.device_id != *device_id {
3818 return Err(StoreProtocolError::DeviceStateMismatch);
3819 }
3820 for (proposal_id, state) in &record.proposals {
3821 let proposal = match state {
3822 StoreDeviceProposalState::Pending { proposal }
3823 | StoreDeviceProposalState::Superseded { proposal, .. } => proposal,
3824 StoreDeviceProposalState::Cancelled { outcome } => &outcome.proposal,
3825 };
3826 if proposal.proposal_id != *proposal_id {
3827 return Err(StoreProtocolError::DeviceStateMismatch);
3828 }
3829 if let StoreDeviceProposalState::Superseded { terminals, .. } = state {
3830 validate_terminal_refs(terminals)?;
3831 }
3832 }
3833 if let StoreDeviceStatus::Inactive { terminals, .. } = &record.status {
3834 validate_terminal_refs(terminals)?;
3835 }
3836 }
3837 Ok(())
3838}
3839
3840fn validate_terminal_refs(terminals: &[StoreDeviceTerminalRef]) -> Result<(), StoreProtocolError> {
3841 if terminals.is_empty() || terminals.windows(2).any(|pair| pair[0] >= pair[1]) {
3842 return Err(StoreProtocolError::DeviceStateMismatch);
3843 }
3844 Ok(())
3845}
3846
3847pub(crate) fn canonical_recovery_cursors(
3848 mut recovery: Vec<OwnerRecoveryCursor>,
3849) -> Result<Vec<OwnerRecoveryCursor>, StoreProtocolError> {
3850 recovery.sort();
3851 validate_recovery_cursors(&recovery)?;
3852 Ok(recovery)
3853}
3854
3855pub(crate) fn validate_recovery_cursors(
3856 recovery: &[OwnerRecoveryCursor],
3857) -> Result<(), StoreProtocolError> {
3858 if recovery.windows(2).any(|pair| pair[0] >= pair[1]) {
3859 return Err(StoreProtocolError::OwnerRecoveryMismatch);
3860 }
3861 Ok(())
3862}
3863
3864impl OwnerRecoveryNodeRef {
3865 pub fn slot(&self) -> &ObjectSlot {
3866 self.object.slot()
3867 }
3868}
3869
3870#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3871#[serde(deny_unknown_fields)]
3872pub struct DeviceRecoveryReadiness {
3873 pub registration: StoreDeviceRegistrationRef,
3874 pub initial_ack: StoreAckRef,
3875 pub bootstrap_cut: StoreHistoryCut,
3876}
3877
3878#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3879#[serde(deny_unknown_fields)]
3880pub struct OwnerRecoveryNode {
3881 pub version: u32,
3882 pub store_root_hash: ObjectHash,
3883 pub recovery_id: DeviceRecoveryId,
3884 pub owner_pubkey: String,
3885 pub owner_grant: MembershipGrantId,
3886 pub sequence: u64,
3887 pub membership: StoreMembershipStateRef,
3888 pub predecessor: Option<OwnerRecoveryNodeRef>,
3889 pub readiness: DeviceRecoveryReadiness,
3890 pub next_slot: ObjectSlot,
3891 pub signature: String,
3892}
3893
3894impl OwnerRecoveryNode {
3895 #[allow(clippy::too_many_arguments)]
3896 pub fn signed(
3897 store_root_hash: ObjectHash,
3898 recovery_id: DeviceRecoveryId,
3899 owner_grant: MembershipGrantId,
3900 sequence: u64,
3901 membership: StoreMembershipStateRef,
3902 predecessor: Option<OwnerRecoveryNodeRef>,
3903 readiness: DeviceRecoveryReadiness,
3904 next_slot: ObjectSlot,
3905 owner_signer: &UserKeypair,
3906 ) -> Result<Self, StoreProtocolError> {
3907 let owner_pubkey = keys::public_key_hex(owner_signer);
3908 let mut node = Self {
3909 version: STORE_PROTOCOL_VERSION,
3910 store_root_hash,
3911 recovery_id,
3912 owner_pubkey,
3913 owner_grant,
3914 sequence,
3915 membership,
3916 predecessor,
3917 readiness,
3918 next_slot,
3919 signature: String::new(),
3920 };
3921 node.validate_shape()?;
3922 let (_, signature) = keys::sign_hex(owner_signer, &node.canonical_signed_bytes());
3923 node.signature = signature;
3924 Ok(node)
3925 }
3926
3927 pub fn parse_at(
3928 bytes: &[u8],
3929 store_root: &StoreRootRef,
3930 reference: &OwnerRecoveryNodeRef,
3931 ) -> Result<Self, StoreProtocolError> {
3932 let node: Self = serde_json::from_slice(bytes)
3933 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
3934 require_version(node.version)?;
3935 node.validate_shape()?;
3936 if node.store_root_hash != store_root.store_root_hash
3937 || node.owner_pubkey != reference.owner_pubkey
3938 || node.owner_grant != reference.owner_grant
3939 || node.sequence != reference.sequence
3940 || node.node_hash() != reference.node_hash
3941 {
3942 return Err(StoreProtocolError::OwnerRecoveryMismatch);
3943 }
3944 if !keys::verify_signature_hex(
3945 &node.owner_pubkey,
3946 &node.signature,
3947 &node.canonical_signed_bytes(),
3948 ) {
3949 return Err(StoreProtocolError::InvalidSignature);
3950 }
3951 Ok(node)
3952 }
3953
3954 fn validate_shape(&self) -> Result<(), StoreProtocolError> {
3955 let predecessor_matches = match &self.predecessor {
3956 None => self.sequence == 1,
3957 Some(predecessor) => {
3958 predecessor.owner_pubkey == self.owner_pubkey
3959 && predecessor.owner_grant == self.owner_grant
3960 && predecessor.sequence.checked_add(1) == Some(self.sequence)
3961 }
3962 };
3963 if !predecessor_matches || self.readiness.initial_ack.revision != 1 {
3964 return Err(StoreProtocolError::OwnerRecoveryMismatch);
3965 }
3966 Ok(())
3967 }
3968
3969 pub(crate) fn canonical_signed_bytes(&self) -> Vec<u8> {
3970 domain_json(
3971 OWNER_RECOVERY_NODE_DOMAIN,
3972 &(
3973 self.version,
3974 self.store_root_hash,
3975 self.recovery_id,
3976 &self.owner_pubkey,
3977 &self.owner_grant,
3978 self.sequence,
3979 &self.membership,
3980 &self.predecessor,
3981 &self.readiness,
3982 &self.next_slot,
3983 ),
3984 )
3985 }
3986
3987 pub fn node_hash(&self) -> ObjectHash {
3988 ObjectHash::digest(&self.canonical_signed_bytes())
3989 }
3990
3991 pub fn to_bytes(&self) -> Vec<u8> {
3992 serde_json::to_vec(self).expect("OwnerRecoveryNode serialization cannot fail")
3993 }
3994}
3995
3996#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3997#[serde(rename_all = "snake_case", deny_unknown_fields)]
3998pub enum StoreDeviceRegistrationOrigin {
3999 Founder {
4000 creation_id: StoreCreationId,
4001 },
4002 Join {
4003 attempt_id: DeviceJoinAttemptId,
4004 attempt_slot: ObjectSlot,
4005 outcome_slot: ObjectSlot,
4006 },
4007 Recovery {
4008 recovery_id: DeviceRecoveryId,
4009 recovery_slot: ObjectSlot,
4010 owner_grant: MembershipGrantId,
4011 },
4012}
4013
4014#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4015#[serde(rename_all = "snake_case", deny_unknown_fields)]
4016pub enum StoreDeviceRegistrationActivation {
4017 Founder {
4018 root: StoreRootRef,
4019 },
4020 Join {
4021 attempt_id: DeviceJoinAttemptId,
4022 outcome: DeviceJoinOutcomeRef,
4023 },
4024 Recovery {
4025 recovery_id: DeviceRecoveryId,
4026 node: OwnerRecoveryNodeRef,
4027 },
4028}
4029
4030#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4031#[serde(deny_unknown_fields)]
4032pub struct ActivatedStoreDeviceRegistrationRef {
4033 pub registration: StoreDeviceRegistrationRef,
4034 pub authority: StoreDeviceRegistrationActivationRef,
4035}
4036
4037#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4038#[serde(rename_all = "snake_case", deny_unknown_fields)]
4039pub enum StoreDeviceRegistrationActivationRef {
4040 Join {
4041 attempt_id: DeviceJoinAttemptId,
4042 outcome: DeviceJoinOutcomeRef,
4043 },
4044 Recovery {
4045 recovery_id: DeviceRecoveryId,
4046 node: OwnerRecoveryNodeRef,
4047 },
4048}
4049
4050impl StoreDeviceRegistrationOrigin {
4051 fn external_id(&self) -> ObjectHash {
4052 match self {
4053 Self::Founder { creation_id } => creation_id.0,
4054 Self::Join { attempt_id, .. } => attempt_id.0,
4055 Self::Recovery { recovery_id, .. } => recovery_id.0,
4056 }
4057 }
4058}
4059
4060#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4061#[serde(rename_all = "snake_case", deny_unknown_fields)]
4062pub enum DeviceStreamAnchor {
4063 StoreAnnouncements {
4064 first_slot: ObjectSlot,
4065 },
4066 StoreAcknowledgements {
4067 first_slot: ObjectSlot,
4068 },
4069 StoreSnapshots {
4070 first_slot: ObjectSlot,
4071 },
4072 CircleAcknowledgements {
4073 circle_id: CircleId,
4074 first_slot: ObjectSlot,
4075 },
4076 CircleSnapshots {
4077 circle_id: CircleId,
4078 first_slot: ObjectSlot,
4079 },
4080}
4081
4082#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4083#[serde(rename_all = "snake_case", deny_unknown_fields)]
4084pub enum GrantStreamAnchor {
4085 StoreMembership { first_slot: ObjectSlot },
4086 OwnerRecovery { first_slot: ObjectSlot },
4087}
4088
4089impl GrantStreamAnchor {
4090 pub fn first_slot(&self) -> &ObjectSlot {
4091 match self {
4092 Self::StoreMembership { first_slot } | Self::OwnerRecovery { first_slot } => first_slot,
4093 }
4094 }
4095}
4096
4097#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4098#[serde(rename_all = "snake_case", deny_unknown_fields)]
4099pub enum StoreCommitAnchor {
4100 MergeConcurrent { announcements: DeviceStreamAnchor },
4101 Serial,
4102}
4103
4104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4105#[serde(deny_unknown_fields)]
4106pub struct StoreDeviceRegistration {
4107 pub version: u32,
4108 pub store_root: StoreRootRef,
4109 pub device_id: StoreDeviceId,
4110 pub author_pubkey: String,
4111 pub device_signing_pubkey: String,
4112 pub origin: StoreDeviceRegistrationOrigin,
4113 pub provider: ProviderDeviceBinding,
4114 pub store_commits: StoreCommitAnchor,
4115 pub acknowledgements: DeviceStreamAnchor,
4116 pub snapshots: DeviceStreamAnchor,
4117 pub identity_signature: String,
4118}
4119
4120#[derive(Serialize)]
4121struct RegistrationSignedFields<'a> {
4122 version: u32,
4123 store_root: &'a StoreRootRef,
4124 device_id: StoreDeviceId,
4125 author_pubkey: &'a str,
4126 device_signing_pubkey: &'a str,
4127 origin: &'a StoreDeviceRegistrationOrigin,
4128 provider: &'a ProviderDeviceBinding,
4129 store_commits: &'a StoreCommitAnchor,
4130 acknowledgements: &'a DeviceStreamAnchor,
4131 snapshots: &'a DeviceStreamAnchor,
4132}
4133
4134impl StoreDeviceRegistration {
4135 pub fn signed(
4136 store_root: StoreRootRef,
4137 origin: StoreDeviceRegistrationOrigin,
4138 provider: ProviderDeviceBinding,
4139 store_commits: StoreCommitAnchor,
4140 acknowledgements: DeviceStreamAnchor,
4141 snapshots: DeviceStreamAnchor,
4142 identity_signer: &UserKeypair,
4143 ) -> Result<Self, StoreProtocolError> {
4144 validate_registration_anchors(&store_commits, &acknowledgements, &snapshots)?;
4145 let author_pubkey = keys::public_key_hex(identity_signer);
4146 let device_signer = derive_device_signer(identity_signer, &store_root, &origin);
4147 let device_signing_pubkey = keys::public_key_hex(&device_signer);
4148 let device_id = StoreDeviceId::derive(&store_root, &origin);
4149 let mut registration = Self {
4150 version: STORE_PROTOCOL_VERSION,
4151 store_root,
4152 device_id,
4153 author_pubkey,
4154 device_signing_pubkey,
4155 origin,
4156 provider,
4157 store_commits,
4158 acknowledgements,
4159 snapshots,
4160 identity_signature: String::new(),
4161 };
4162 let (_, signature) =
4163 keys::sign_hex(identity_signer, ®istration.canonical_signed_bytes());
4164 registration.identity_signature = signature;
4165 Ok(registration)
4166 }
4167
4168 pub(crate) fn device_signer(
4169 &self,
4170 identity_signer: &UserKeypair,
4171 ) -> Result<UserKeypair, StoreProtocolError> {
4172 if keys::public_key_hex(identity_signer) != self.author_pubkey {
4173 return Err(StoreProtocolError::InvalidSignature);
4174 }
4175 let signer = derive_device_signer(identity_signer, &self.store_root, &self.origin);
4176 if keys::public_key_hex(&signer) != self.device_signing_pubkey {
4177 return Err(StoreProtocolError::InvalidSignature);
4178 }
4179 Ok(signer)
4180 }
4181
4182 fn canonical_signed_bytes(&self) -> Vec<u8> {
4183 domain_json(
4184 REGISTRATION_DOMAIN,
4185 &RegistrationSignedFields {
4186 version: self.version,
4187 store_root: &self.store_root,
4188 device_id: self.device_id,
4189 author_pubkey: &self.author_pubkey,
4190 device_signing_pubkey: &self.device_signing_pubkey,
4191 origin: &self.origin,
4192 provider: &self.provider,
4193 store_commits: &self.store_commits,
4194 acknowledgements: &self.acknowledgements,
4195 snapshots: &self.snapshots,
4196 },
4197 )
4198 }
4199
4200 pub fn to_bytes(&self) -> Vec<u8> {
4201 serde_json::to_vec(self).expect("StoreDeviceRegistration serialization cannot fail")
4202 }
4203
4204 pub fn registration_hash(&self) -> ObjectHash {
4205 ObjectHash::digest(&self.canonical_signed_bytes())
4206 }
4207
4208 pub fn parse_at(
4209 bytes: &[u8],
4210 expected_store_root: &StoreRootRef,
4211 expected_device: StoreDeviceId,
4212 ) -> Result<Self, StoreProtocolError> {
4213 let registration: Self = serde_json::from_slice(bytes)
4214 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
4215 require_version(registration.version)?;
4216 if ®istration.store_root != expected_store_root {
4217 return Err(StoreProtocolError::StoreRootMismatch {
4218 expected: expected_store_root.store_root_hash,
4219 actual: registration.store_root.store_root_hash,
4220 });
4221 }
4222 if registration.device_id != expected_device {
4223 return Err(StoreProtocolError::RelocatedSlot {
4224 expected: registration_slot_prefix(&expected_device.to_string()),
4225 actual: registration_slot_prefix(®istration.device_id.to_string()),
4226 });
4227 }
4228 if registration.device_id
4229 != StoreDeviceId::derive(®istration.store_root, ®istration.origin)
4230 {
4231 return Err(StoreProtocolError::Malformed(
4232 "Store device id differs from its root and origin".to_string(),
4233 ));
4234 }
4235 validate_registration_anchors(
4236 ®istration.store_commits,
4237 ®istration.acknowledgements,
4238 ®istration.snapshots,
4239 )?;
4240 if !keys::verify_signature_hex(
4241 ®istration.author_pubkey,
4242 ®istration.identity_signature,
4243 ®istration.canonical_signed_bytes(),
4244 ) {
4245 return Err(StoreProtocolError::InvalidSignature);
4246 }
4247 Ok(registration)
4248 }
4249}
4250
4251fn derive_device_signer(
4252 identity_signer: &UserKeypair,
4253 store_root: &StoreRootRef,
4254 origin: &StoreDeviceRegistrationOrigin,
4255) -> UserKeypair {
4256 const DOMAIN: &[u8] = b"coven.store-device-signing-key.v1\0";
4257 let context = serde_json::to_vec(&(store_root, origin))
4258 .expect("Store device signing context serialization cannot fail");
4259 identity_signer.derive_signing_key(DOMAIN, &context)
4260}
4261
4262fn validate_registration_anchors(
4263 commits: &StoreCommitAnchor,
4264 acknowledgements: &DeviceStreamAnchor,
4265 snapshots: &DeviceStreamAnchor,
4266) -> Result<(), StoreProtocolError> {
4267 if !matches!(
4268 acknowledgements,
4269 DeviceStreamAnchor::StoreAcknowledgements { .. }
4270 ) || !matches!(snapshots, DeviceStreamAnchor::StoreSnapshots { .. })
4271 || !matches!(
4272 commits,
4273 StoreCommitAnchor::MergeConcurrent {
4274 announcements: DeviceStreamAnchor::StoreAnnouncements { .. }
4275 }
4276 ) && !matches!(commits, StoreCommitAnchor::Serial)
4277 {
4278 return Err(StoreProtocolError::Malformed(
4279 "Store device registration contains mismatched permanent stream anchors".to_string(),
4280 ));
4281 }
4282 Ok(())
4283}
4284
4285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4286#[serde(deny_unknown_fields)]
4287pub struct StoreAck {
4288 pub version: u32,
4289 pub store_root_hash: ObjectHash,
4290 pub author_registration: StoreDeviceRegistrationRef,
4291 pub revision: u64,
4292 pub predecessor: Option<StoreAckRef>,
4293 pub store_cut: StoreHistoryCut,
4294 pub last_sync: String,
4295 pub successor: SuccessorLink,
4296 pub signature: String,
4297}
4298
4299#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
4300#[serde(deny_unknown_fields)]
4301pub struct StoreAckRef {
4302 pub revision: u64,
4303 pub ack_hash: ObjectHash,
4304 pub object: ExactObjectRef,
4305}
4306
4307#[derive(Serialize)]
4308struct AckSignedFields<'a> {
4309 version: u32,
4310 store_root_hash: ObjectHash,
4311 author_registration: &'a StoreDeviceRegistrationRef,
4312 revision: u64,
4313 predecessor: Option<&'a StoreAckRef>,
4314 store_cut: &'a StoreHistoryCut,
4315 last_sync: &'a str,
4316 successor: &'a SuccessorLink,
4317}
4318
4319impl StoreAck {
4320 pub fn signed(
4321 store_root_hash: ObjectHash,
4322 author_registration: StoreDeviceRegistrationRef,
4323 revision: u64,
4324 predecessor: Option<StoreAckRef>,
4325 store_cut: StoreHistoryCut,
4326 last_sync: String,
4327 successor: SuccessorLink,
4328 device_signer: &UserKeypair,
4329 ) -> Result<Self, StoreProtocolError> {
4330 validate_chained_revision(revision, predecessor.as_ref().map(|value| value.ack_hash))?;
4331 validate_store_history_cut(&store_cut)?;
4332 validate_ack_history_cut(store_root_hash, &author_registration, &store_cut)?;
4333 let mut ack = Self {
4334 version: STORE_PROTOCOL_VERSION,
4335 store_root_hash,
4336 author_registration,
4337 revision,
4338 predecessor,
4339 store_cut,
4340 last_sync,
4341 successor,
4342 signature: String::new(),
4343 };
4344 let (_, signature) = keys::sign_hex(device_signer, &ack.canonical_signed_bytes());
4345 ack.signature = signature;
4346 Ok(ack)
4347 }
4348
4349 fn canonical_signed_bytes(&self) -> Vec<u8> {
4350 domain_json(
4351 ACK_DOMAIN,
4352 &AckSignedFields {
4353 version: self.version,
4354 store_root_hash: self.store_root_hash,
4355 author_registration: &self.author_registration,
4356 revision: self.revision,
4357 predecessor: self.predecessor.as_ref(),
4358 store_cut: &self.store_cut,
4359 last_sync: &self.last_sync,
4360 successor: &self.successor,
4361 },
4362 )
4363 }
4364
4365 pub fn to_bytes(&self) -> Vec<u8> {
4366 serde_json::to_vec(self).expect("StoreAck serialization cannot fail")
4367 }
4368
4369 pub fn ack_hash(&self) -> ObjectHash {
4370 ObjectHash::digest(&self.canonical_signed_bytes())
4371 }
4372
4373 pub fn semantic_hash_from_bytes(bytes: &[u8]) -> Result<ObjectHash, StoreProtocolError> {
4374 let ack: Self = serde_json::from_slice(bytes)
4375 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
4376 Ok(ack.ack_hash())
4377 }
4378
4379 pub fn parse_at(
4380 bytes: &[u8],
4381 expected_store_root_hash: ObjectHash,
4382 expected: &StoreAckRef,
4383 author: &StoreDeviceRegistration,
4384 ) -> Result<Self, StoreProtocolError> {
4385 let ack: Self = serde_json::from_slice(bytes)
4386 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
4387 require_version(ack.version)?;
4388 if ack.store_root_hash != expected_store_root_hash {
4389 return Err(StoreProtocolError::StoreRootMismatch {
4390 expected: expected_store_root_hash,
4391 actual: ack.store_root_hash,
4392 });
4393 }
4394 ack.author_registration.verify_registration(author)?;
4395 if ack.revision != expected.revision {
4396 return Err(StoreProtocolError::RelocatedSlot {
4397 expected: ack_slot_prefix(&author.device_id.to_string(), expected.revision),
4398 actual: ack_slot_prefix(&author.device_id.to_string(), ack.revision),
4399 });
4400 }
4401 validate_chained_revision(
4402 ack.revision,
4403 ack.predecessor.as_ref().map(|value| value.ack_hash),
4404 )?;
4405 validate_store_history_cut(&ack.store_cut)?;
4406 validate_ack_history_cut(
4407 ack.store_root_hash,
4408 &ack.author_registration,
4409 &ack.store_cut,
4410 )?;
4411 if !keys::verify_signature_hex(
4412 &author.device_signing_pubkey,
4413 &ack.signature,
4414 &ack.canonical_signed_bytes(),
4415 ) {
4416 return Err(StoreProtocolError::InvalidSignature);
4417 }
4418 if ack.ack_hash() != expected.ack_hash {
4419 return Err(StoreProtocolError::ObjectHashMismatch {
4420 expected: expected.ack_hash,
4421 actual: ack.ack_hash(),
4422 });
4423 }
4424 Ok(ack)
4425 }
4426}
4427
4428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4429#[serde(deny_unknown_fields)]
4430pub struct SnapshotMeta {
4431 pub version: u32,
4432 pub store_root_hash: ObjectHash,
4433 pub author_registration: StoreDeviceRegistrationRef,
4434 pub sequence: u64,
4435 pub predecessor: Option<StoreSnapshotRef>,
4436 pub image: SnapshotImageRef,
4437 pub coverage: CommitFrontier,
4438 pub schema_version: u32,
4439 pub created_at: String,
4440 pub successor: SuccessorLink,
4441 pub signature: String,
4442}
4443
4444#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
4445#[serde(deny_unknown_fields)]
4446pub struct SnapshotImageRef {
4447 pub image_hash: ObjectHash,
4448 pub object: ExactObjectRef,
4449}
4450
4451#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
4452#[serde(deny_unknown_fields)]
4453pub struct StoreSnapshotRef {
4454 pub sequence: u64,
4455 pub snapshot_hash: ObjectHash,
4456 pub object: ExactObjectRef,
4457}
4458
4459#[derive(Serialize)]
4460struct SnapshotSignedFields<'a> {
4461 version: u32,
4462 store_root_hash: ObjectHash,
4463 author_registration: &'a StoreDeviceRegistrationRef,
4464 sequence: u64,
4465 predecessor: Option<&'a StoreSnapshotRef>,
4466 image: &'a SnapshotImageRef,
4467 coverage: &'a CommitFrontier,
4468 schema_version: u32,
4469 created_at: &'a str,
4470 successor: &'a SuccessorLink,
4471}
4472
4473impl SnapshotMeta {
4474 pub fn signed(
4475 store_root_hash: ObjectHash,
4476 author_registration: StoreDeviceRegistrationRef,
4477 sequence: u64,
4478 predecessor: Option<StoreSnapshotRef>,
4479 image: SnapshotImageRef,
4480 coverage: CommitFrontier,
4481 schema_version: u32,
4482 created_at: String,
4483 successor: SuccessorLink,
4484 device_signer: &UserKeypair,
4485 ) -> Result<Self, StoreProtocolError> {
4486 validate_chained_revision(
4487 sequence,
4488 predecessor.as_ref().map(|value| value.snapshot_hash),
4489 )?;
4490 validate_commit_frontier(&coverage)?;
4491 let mut meta = Self {
4492 version: STORE_PROTOCOL_VERSION,
4493 store_root_hash,
4494 author_registration,
4495 sequence,
4496 predecessor,
4497 image,
4498 coverage,
4499 schema_version,
4500 created_at,
4501 successor,
4502 signature: String::new(),
4503 };
4504 let (_, signature) = keys::sign_hex(device_signer, &meta.canonical_signed_bytes());
4505 meta.signature = signature;
4506 Ok(meta)
4507 }
4508
4509 fn canonical_signed_bytes(&self) -> Vec<u8> {
4510 domain_json(
4511 SNAPSHOT_DOMAIN,
4512 &SnapshotSignedFields {
4513 version: self.version,
4514 store_root_hash: self.store_root_hash,
4515 author_registration: &self.author_registration,
4516 sequence: self.sequence,
4517 predecessor: self.predecessor.as_ref(),
4518 image: &self.image,
4519 coverage: &self.coverage,
4520 schema_version: self.schema_version,
4521 created_at: &self.created_at,
4522 successor: &self.successor,
4523 },
4524 )
4525 }
4526
4527 pub fn snapshot_hash(&self) -> ObjectHash {
4528 ObjectHash::digest(&self.canonical_signed_bytes())
4529 }
4530
4531 pub fn semantic_hash_from_bytes(bytes: &[u8]) -> Result<ObjectHash, StoreProtocolError> {
4532 let meta: Self = serde_json::from_slice(bytes)
4533 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
4534 Ok(meta.snapshot_hash())
4535 }
4536
4537 pub fn to_bytes(&self) -> Vec<u8> {
4538 serde_json::to_vec(self).expect("SnapshotMeta serialization cannot fail")
4539 }
4540
4541 pub fn parse_at(
4542 bytes: &[u8],
4543 expected_store_root_hash: ObjectHash,
4544 expected: &StoreSnapshotRef,
4545 author: &StoreDeviceRegistration,
4546 ) -> Result<Self, StoreProtocolError> {
4547 let meta: Self = serde_json::from_slice(bytes)
4548 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
4549 require_version(meta.version)?;
4550 if meta.store_root_hash != expected_store_root_hash {
4551 return Err(StoreProtocolError::StoreRootMismatch {
4552 expected: expected_store_root_hash,
4553 actual: meta.store_root_hash,
4554 });
4555 }
4556 meta.author_registration.verify_registration(author)?;
4557 if meta.sequence != expected.sequence {
4558 return Err(StoreProtocolError::RelocatedSlot {
4559 expected: snapshot_semantic_prefix(
4560 &author.device_id.to_string(),
4561 expected.snapshot_hash,
4562 ),
4563 actual: snapshot_semantic_prefix(
4564 &author.device_id.to_string(),
4565 meta.snapshot_hash(),
4566 ),
4567 });
4568 }
4569 validate_chained_revision(
4570 meta.sequence,
4571 meta.predecessor.as_ref().map(|value| value.snapshot_hash),
4572 )?;
4573 validate_commit_frontier(&meta.coverage)?;
4574 if !keys::verify_signature_hex(
4575 &author.device_signing_pubkey,
4576 &meta.signature,
4577 &meta.canonical_signed_bytes(),
4578 ) {
4579 return Err(StoreProtocolError::InvalidSignature);
4580 }
4581 let actual = meta.snapshot_hash();
4582 if actual != expected.snapshot_hash {
4583 return Err(StoreProtocolError::ObjectHashMismatch {
4584 expected: expected.snapshot_hash,
4585 actual,
4586 });
4587 }
4588 Ok(meta)
4589 }
4590}
4591
4592#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4593#[serde(deny_unknown_fields)]
4594pub struct StoreCreationDescriptor {
4595 pub version: u32,
4596 pub creation_id: StoreCreationId,
4597 pub provider: super::storage::StoreProviderBinding,
4598 pub schema_version: u32,
4599 pub sync_routing_hash: ObjectHash,
4600 pub write_policy: WritePolicy,
4601 pub founder_pubkey: String,
4602 pub founder_grant: MembershipGrantId,
4603 pub root_slot: ObjectSlot,
4604 pub founder_registration: ObjectSlot,
4605 pub founder_provider_admin: super::provider::FounderProviderAdminGrant,
4606 pub membership: StoreMembershipGenesis,
4607 pub founder_recovery: GrantStreamAnchor,
4608}
4609
4610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4611#[serde(rename_all = "snake_case", deny_unknown_fields)]
4612pub enum StoreMembershipGenesis {
4613 MergeConcurrent {
4614 founder_membership: GrantStreamAnchor,
4615 },
4616 Serial,
4617}
4618
4619impl StoreCreationDescriptor {
4620 pub fn store_root_id(&self) -> ObjectHash {
4621 ObjectHash::digest(&domain_json(b"coven.store-creation-descriptor.v1\0", self))
4622 }
4623
4624 pub fn validate_merge_founder_entry(
4625 &self,
4626 founder: &MembershipEntry,
4627 ) -> Result<(), StoreProtocolError> {
4628 let StoreMembershipGenesis::MergeConcurrent { founder_membership } = &self.membership
4629 else {
4630 return Err(StoreProtocolError::InvalidFounder);
4631 };
4632 let MembershipChange::Founder {
4633 owner_pubkey,
4634 owner_grant_id,
4635 membership,
4636 provider_admin,
4637 } = &founder.change
4638 else {
4639 return Err(StoreProtocolError::InvalidFounder);
4640 };
4641 if founder.store_id != self.store_root_id().to_string()
4642 || founder.author_pubkey != self.founder_pubkey
4643 || founder.author_owner_grant != self.founder_grant
4644 || owner_pubkey != &self.founder_pubkey
4645 || owner_grant_id != &self.founder_grant
4646 || membership != founder_membership
4647 || provider_admin != &self.founder_provider_admin
4648 || founder.seq != 1
4649 || founder.previous_hash.is_some()
4650 || !founder.dependencies.is_empty()
4651 || !founder.resolution_dependencies.is_empty()
4652 || founder.provider_admin.is_some()
4653 || !verify_membership_entry(founder)
4654 {
4655 return Err(StoreProtocolError::InvalidFounder);
4656 }
4657 Ok(())
4658 }
4659}
4660
4661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4662#[serde(deny_unknown_fields)]
4663pub struct StoreProtocolRoot {
4664 pub descriptor: StoreCreationDescriptor,
4665 pub signature: String,
4666}
4667
4668impl StoreProtocolRoot {
4669 pub fn signed(
4670 descriptor: StoreCreationDescriptor,
4671 signer: &UserKeypair,
4672 ) -> Result<Self, StoreProtocolError> {
4673 let mut store_protocol_root = Self {
4674 descriptor,
4675 signature: String::new(),
4676 };
4677 store_protocol_root.validate_descriptor()?;
4678 if keys::public_key_hex(signer) != store_protocol_root.descriptor.founder_pubkey {
4679 return Err(StoreProtocolError::InvalidSignature);
4680 }
4681 let (_, signature) = keys::sign_hex(signer, &store_protocol_root.canonical_signed_bytes());
4682 store_protocol_root.signature = signature;
4683 Ok(store_protocol_root)
4684 }
4685
4686 fn canonical_signed_bytes(&self) -> Vec<u8> {
4687 domain_json(STORE_PROTOCOL_ROOT_DOMAIN, &self.descriptor)
4688 }
4689
4690 pub fn to_bytes(&self) -> Vec<u8> {
4691 serde_json::to_vec(self).expect("StoreProtocolRoot serialization cannot fail")
4692 }
4693
4694 pub fn object_hash(&self) -> ObjectHash {
4695 ObjectHash::digest(&self.to_bytes())
4696 }
4697
4698 pub fn parse(bytes: &[u8]) -> Result<Self, StoreProtocolError> {
4699 let store_protocol_root: Self = serde_json::from_slice(bytes)
4700 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
4701 require_version(store_protocol_root.descriptor.version)?;
4702 store_protocol_root.validate_descriptor()?;
4703 if !keys::verify_signature_hex(
4704 &store_protocol_root.descriptor.founder_pubkey,
4705 &store_protocol_root.signature,
4706 &store_protocol_root.canonical_signed_bytes(),
4707 ) {
4708 return Err(StoreProtocolError::InvalidSignature);
4709 }
4710 Ok(store_protocol_root)
4711 }
4712
4713 pub fn parse_expected(
4714 bytes: &[u8],
4715 expected: &StoreRootRef,
4716 expected_write_policy: WritePolicy,
4717 expected_sync_routing_hash: ObjectHash,
4718 ) -> Result<Self, StoreProtocolError> {
4719 let store_protocol_root = Self::parse_pinned(bytes, expected)?;
4720 if store_protocol_root.descriptor.write_policy != expected_write_policy {
4721 return Err(StoreProtocolError::WritePolicyMismatch {
4722 expected: expected_write_policy,
4723 actual: store_protocol_root.descriptor.write_policy,
4724 });
4725 }
4726 if store_protocol_root.descriptor.sync_routing_hash != expected_sync_routing_hash {
4727 return Err(StoreProtocolError::SyncRoutingMismatch {
4728 expected: expected_sync_routing_hash,
4729 actual: store_protocol_root.descriptor.sync_routing_hash,
4730 });
4731 }
4732 Ok(store_protocol_root)
4733 }
4734
4735 pub fn parse_pinned(bytes: &[u8], expected: &StoreRootRef) -> Result<Self, StoreProtocolError> {
4736 let store_protocol_root = Self::parse(bytes)?;
4737 let actual_hash = store_protocol_root.object_hash();
4738 if actual_hash != expected.store_root_hash {
4739 return Err(StoreProtocolError::StoreRootMismatch {
4740 expected: expected.store_root_hash,
4741 actual: actual_hash,
4742 });
4743 }
4744 let actual_root_id = store_protocol_root.descriptor.store_root_id();
4745 if actual_root_id != expected.store_root_id {
4746 return Err(StoreProtocolError::StoreRootIdMismatch {
4747 expected: expected.store_root_id,
4748 actual: actual_root_id,
4749 });
4750 }
4751 if expected.object.slot() != &store_protocol_root.descriptor.root_slot {
4752 return Err(StoreProtocolError::RelocatedSlot {
4753 expected: serde_json::to_string(&store_protocol_root.descriptor.root_slot)
4754 .expect("Store root slot serialization cannot fail"),
4755 actual: serde_json::to_string(expected.object.slot())
4756 .expect("Store root slot serialization cannot fail"),
4757 });
4758 }
4759 Ok(store_protocol_root)
4760 }
4761
4762 fn validate_descriptor(&self) -> Result<(), StoreProtocolError> {
4763 let descriptor = &self.descriptor;
4764 descriptor
4765 .provider
4766 .validate()
4767 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
4768 descriptor
4769 .founder_provider_admin
4770 .provider
4771 .validate_for(&descriptor.provider)
4772 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
4773 descriptor
4774 .founder_provider_admin
4775 .capability
4776 .verify(
4777 &descriptor.provider,
4778 &descriptor.founder_provider_admin.provider,
4779 descriptor.write_policy == WritePolicy::Serial,
4780 )
4781 .map_err(|error| StoreProtocolError::Malformed(error.to_string()))?;
4782 if !matches!(
4783 descriptor.founder_recovery,
4784 GrantStreamAnchor::OwnerRecovery { .. }
4785 ) {
4786 return Err(StoreProtocolError::InvalidFounder);
4787 }
4788 if descriptor.version != STORE_PROTOCOL_VERSION
4789 || descriptor.founder_pubkey.is_empty()
4790 || descriptor.root_slot.logical_key() != "store-v1/store-protocol-root.json"
4791 || !matches!(
4792 (&descriptor.write_policy, &descriptor.membership),
4793 (
4794 WritePolicy::MergeConcurrent,
4795 StoreMembershipGenesis::MergeConcurrent {
4796 founder_membership: GrantStreamAnchor::StoreMembership { .. }
4797 }
4798 ) | (WritePolicy::Serial, StoreMembershipGenesis::Serial)
4799 )
4800 {
4801 return Err(StoreProtocolError::InvalidFounder);
4802 }
4803 Ok(())
4804 }
4805}
4806
4807#[derive(Debug, thiserror::Error, PartialEq, Eq)]
4808pub enum StoreProtocolError {
4809 #[error("object hash must be exactly 64 lowercase hexadecimal characters: {0:?}")]
4810 InvalidObjectHash(String),
4811 #[error("unsupported Store protocol version {0}")]
4812 UnsupportedVersion(u32),
4813 #[error("malformed Store protocol object: {0}")]
4814 Malformed(String),
4815 #[error("Store protocol signature is invalid")]
4816 InvalidSignature,
4817 #[error("Store protocol object is in slot {actual:?}, expected {expected:?}")]
4818 RelocatedSlot { expected: String, actual: String },
4819 #[error("Store package names key {actual:?}, expected {expected:?}")]
4820 RelocatedPackage { expected: String, actual: String },
4821 #[error("candidate object names key {actual:?}, expected {expected:?}")]
4822 RelocatedCandidateObject { expected: String, actual: String },
4823 #[error("Store protocol root hash is {actual}, expected {expected}")]
4824 StoreRootMismatch {
4825 expected: ObjectHash,
4826 actual: ObjectHash,
4827 },
4828 #[error("Store protocol root id is {actual}, expected {expected}")]
4829 StoreRootIdMismatch {
4830 expected: ObjectHash,
4831 actual: ObjectHash,
4832 },
4833 #[error("Store id is {actual:?}, expected {expected:?}")]
4834 StoreMismatch { expected: String, actual: String },
4835 #[error("founder is {actual:?}, expected {expected:?}")]
4836 FounderMismatch { expected: String, actual: String },
4837 #[error("store protocol root has an invalid founder membership entry")]
4838 InvalidFounder,
4839 #[error("Store write policy is {actual:?}, expected {expected:?}")]
4840 WritePolicyMismatch {
4841 expected: WritePolicy,
4842 actual: WritePolicy,
4843 },
4844 #[error("Store sync-routing hash is {actual}, expected {expected}")]
4845 SyncRoutingMismatch {
4846 expected: ObjectHash,
4847 actual: ObjectHash,
4848 },
4849 #[error("Store controls require the Serial write policy")]
4850 ControlRequiresSerial,
4851 #[error("Store Serial control is invalid or signed by a different commit author")]
4852 InvalidSerialControl,
4853 #[error("Store batch has no Store package, circle package, or control")]
4854 EmptyBatch,
4855 #[error("Store batch has no Store package")]
4856 MissingStorePackage,
4857 #[error("Store batch repeats Store device registration {device_id:?} revision {revision}")]
4858 DuplicateDeviceRegistration { device_id: String, revision: u64 },
4859 #[error(
4860 "Store device registration {device_id:?} revision {revision} has hash {actual}, expected {expected}"
4861 )]
4862 DeviceRegistrationRefMismatch {
4863 device_id: String,
4864 revision: u64,
4865 expected: ObjectHash,
4866 actual: ObjectHash,
4867 },
4868 #[error("device join attempt fields do not name one exact registration lifecycle")]
4869 JoinAttemptMismatch,
4870 #[error("device readiness proof differs from its exact attempt, registration, or initial acknowledgement")]
4871 DeviceReadinessMismatch,
4872 #[error("device join outcome differs from its exact attempt or closed outcome variant")]
4873 JoinOutcomeMismatch,
4874 #[error("provider access activation contains duplicate or contradictory exact authority")]
4875 ProviderAccessMismatch,
4876 #[error("Owner recovery node differs from its exact registration lifecycle")]
4877 OwnerRecoveryMismatch,
4878 #[error("Store device state differs from its signed predecessor state")]
4879 DeviceStateMismatch,
4880 #[error("Store batch has no package for circle {0}")]
4881 MissingCirclePackage(CircleId),
4882 #[error("Store batch has more than one package for circle {0}")]
4883 DuplicateCirclePackage(CircleId),
4884 #[error("Store batch has more than one control for circle {0}")]
4885 DuplicateCircleControl(CircleId),
4886 #[error("circle control coordinate is invalid")]
4887 InvalidCircleControlCoord,
4888 #[error("circle control uses {actual:?}, expected Store policy {expected:?}")]
4889 CircleControlPolicyMismatch {
4890 expected: WritePolicy,
4891 actual: WritePolicy,
4892 },
4893 #[error("circle {circle_id} package is at {actual:?}, expected {expected:?}")]
4894 RelocatedCirclePackage {
4895 circle_id: CircleId,
4896 expected: String,
4897 actual: String,
4898 },
4899 #[error("Store key generation must be positive, got {0}")]
4900 InvalidKeyGeneration(u64),
4901 #[error("Serial head commit and tip write id must either both be present or both be absent")]
4902 InvalidSerialHead,
4903 #[error("store protocol root store id is empty")]
4904 EmptyStoreId,
4905 #[error("Store commit sequence must start at 1, got {0}")]
4906 InvalidSequence(u64),
4907 #[error("Store commit sequence 1 must not name a predecessor")]
4908 UnexpectedPredecessor,
4909 #[error("Store commit after sequence 1 must name its predecessor hash")]
4910 MissingPredecessor,
4911 #[error("Store control revision must start at 1, got {0}")]
4912 InvalidRevision(u64),
4913 #[error("Store control revision 1 must not name a predecessor")]
4914 UnexpectedControlPredecessor,
4915 #[error("Store control revision after 1 must name its predecessor hash")]
4916 MissingControlPredecessor,
4917 #[error("Store commit for {0:?} must not name its own device as a dependency")]
4918 OwnDependency(String),
4919 #[error(
4920 "invalid membership coordinate {author}/{grant}/{stream_id}/{seq} with entry hash {entry_hash}"
4921 )]
4922 InvalidMembershipCoordinate {
4923 author: String,
4924 grant: String,
4925 stream_id: String,
4926 seq: u64,
4927 entry_hash: String,
4928 },
4929 #[error("invalid Store membership resolution authority for resolver {0:?}")]
4930 InvalidMembershipResolutionAuthority(String),
4931 #[error("membership object coordinate {expected:?} differs from signed entry {declared:?}")]
4932 MembershipCoordinateMismatch {
4933 expected: Box<MembershipCoord>,
4934 declared: Box<MembershipCoord>,
4935 },
4936 #[error("Store package length exceeds the platform address space")]
4937 PackageTooLarge,
4938 #[error("Store package length is {actual}, expected {expected}")]
4939 PackageLengthMismatch { expected: u64, actual: u64 },
4940 #[error("Store package hash is {actual}, expected {expected}")]
4941 PackageHashMismatch {
4942 expected: ObjectHash,
4943 actual: ObjectHash,
4944 },
4945 #[error("Store object hash is {actual}, expected {expected}")]
4946 ObjectHashMismatch {
4947 expected: ObjectHash,
4948 actual: ObjectHash,
4949 },
4950}
4951
4952pub fn protocol_prefix() -> &'static str {
4953 STORE_PROTOCOL_PREFIX
4954}
4955
4956pub fn serial_head_key() -> &'static str {
4957 STORE_SERIAL_HEAD_KEY
4958}
4959
4960pub fn store_protocol_root_logical_key() -> &'static str {
4961 STORE_PROTOCOL_ROOT_SEMANTIC_PATH
4962}
4963
4964pub fn device_join_attempt_semantic_prefix(attempt_id: DeviceJoinAttemptId) -> String {
4965 format!("{STORE_DEVICE_JOIN_ATTEMPT_PREFIX}{}", attempt_id.0)
4966}
4967
4968pub fn device_self_retirement_semantic_prefix(
4969 family: CandidateFamilyId,
4970 device_id: &StoreDeviceId,
4971 retirement_hash: ObjectHash,
4972) -> String {
4973 format!(
4974 "{STORE_CANDIDATE_PREFIX}{}/device-self-retirements/{device_id}/{retirement_hash}",
4975 family.as_hash()
4976 )
4977}
4978
4979pub fn circle_access_leaf_semantic_prefix(
4980 circle_id: CircleId,
4981 family: CandidateFamilyId,
4982 owner_pubkey: &str,
4983 epoch_id: CircleEpochId,
4984 recipient_slot: &str,
4985 leaf_id: AccessLeafId,
4986) -> String {
4987 format!(
4988 "circles/{circle_id}/candidates/{}/access-leaves/{owner_pubkey}/{epoch_id}/{recipient_slot}/{leaf_id}",
4989 family.as_hash(),
4990 )
4991}
4992
4993pub fn circle_access_envelope_semantic_prefix(
4994 circle_id: CircleId,
4995 family: CandidateFamilyId,
4996 owner_pubkey: &str,
4997 recipient_slot: &str,
4998 control_hash: ObjectHash,
4999) -> String {
5000 format!(
5001 "circles/{circle_id}/candidates/{}/access-envelopes/{owner_pubkey}/{recipient_slot}/{control_hash}",
5002 family.as_hash(),
5003 )
5004}
5005
5006pub fn device_join_outcome_semantic_prefix(attempt_id: DeviceJoinAttemptId) -> String {
5007 format!("{STORE_DEVICE_JOIN_OUTCOME_PREFIX}{}", attempt_id.0)
5008}
5009
5010pub fn device_join_abandonment_semantic_prefix(attempt_id: DeviceJoinAttemptId) -> String {
5011 device_join_attempt_semantic_prefix(attempt_id)
5012}
5013
5014pub fn device_join_cleanup_receipt_semantic_prefix(attempt_id: DeviceJoinAttemptId) -> String {
5015 format!("{STORE_DEVICE_JOIN_CLEANUP_RECEIPT_PREFIX}{}", attempt_id.0)
5016}
5017
5018pub fn provider_access_grant_semantic_prefix(
5019 grant_id: &super::provider::ProviderAccessGrantId,
5020) -> String {
5021 format!("{STORE_PROVIDER_ACCESS_GRANT_PREFIX}{}", grant_id.0)
5022}
5023
5024pub fn provider_access_withdrawal_semantic_prefix(
5025 grant_id: &super::provider::ProviderAccessGrantId,
5026) -> String {
5027 format!("{STORE_PROVIDER_ACCESS_WITHDRAWAL_PREFIX}{}", grant_id.0)
5028}
5029
5030pub fn owner_recovery_semantic_prefix(
5031 owner_pubkey: &str,
5032 owner_grant: MembershipGrantId,
5033 sequence: u64,
5034) -> String {
5035 format!("{STORE_OWNER_RECOVERY_PREFIX}{owner_pubkey}/{owner_grant}/{sequence}")
5036}
5037
5038pub fn package_semantic_prefix(
5039 family: CandidateFamilyId,
5040 device_id: &str,
5041 seq: u64,
5042 package_hash: ObjectHash,
5043) -> String {
5044 format!(
5045 "{STORE_CANDIDATE_PREFIX}{}/packages/{device_id}/{seq}/{package_hash}",
5046 family.as_hash()
5047 )
5048}
5049
5050pub fn circle_package_semantic_prefix(
5051 circle_id: CircleId,
5052 family: CandidateFamilyId,
5053 device_id: &str,
5054 seq: u64,
5055 package_hash: ObjectHash,
5056) -> String {
5057 format!(
5058 "circles/{circle_id}/candidates/{}/packages/{device_id}/{seq}/{package_hash}",
5059 family.as_hash()
5060 )
5061}
5062
5063pub fn commit_slot_prefix(device_id: &str, seq: u64) -> String {
5064 format!("{STORE_CANDIDATE_PREFIX}*/commits/{device_id}/{seq}")
5065}
5066
5067pub fn commit_semantic_prefix(
5068 family: CandidateFamilyId,
5069 device_id: &str,
5070 seq: u64,
5071 commit_hash: ObjectHash,
5072) -> String {
5073 format!(
5074 "{STORE_CANDIDATE_PREFIX}{}/commits/{device_id}/{seq}/{commit_hash}",
5075 family.as_hash()
5076 )
5077}
5078
5079pub fn semantic_prefix_from_exact_object(
5080 object: &ExactObjectRef,
5081 extension: &str,
5082) -> Result<String, StoreProtocolError> {
5083 object
5084 .slot()
5085 .logical_key()
5086 .strip_suffix(extension)
5087 .map(str::to_string)
5088 .ok_or_else(|| StoreProtocolError::RelocatedSlot {
5089 expected: format!("candidate object ending in {extension}"),
5090 actual: object.slot().logical_key().to_string(),
5091 })
5092}
5093
5094pub fn head_slot_prefix(device_id: &str, seq: u64) -> String {
5095 format!("{STORE_HEAD_PREFIX}{device_id}/{seq}")
5096}
5097
5098pub fn head_semantic_prefix(device_id: &str, seq: u64, head_hash: ObjectHash) -> String {
5099 format!("{}/{head_hash}", head_slot_prefix(device_id, seq))
5100}
5101
5102pub fn registration_slot_prefix(device_id: &str) -> String {
5103 format!("{STORE_DEVICE_REGISTRATION_PREFIX}{device_id}")
5104}
5105
5106pub fn registration_semantic_prefix(device_id: &str) -> String {
5107 registration_slot_prefix(device_id)
5108}
5109
5110pub fn founder_registration_semantic_prefix(creation_id: StoreCreationId) -> String {
5111 format!("store-v1/devices/founder/{creation_id}/registration")
5112}
5113
5114pub fn founder_membership_head_semantic_prefix(creation_id: StoreCreationId) -> String {
5115 format!("{STORE_MEMBERSHIP_HEAD_PREFIX}founder/{creation_id}/1")
5116}
5117
5118pub fn ack_slot_prefix(device_id: &str, revision: u64) -> String {
5119 format!("{STORE_ACK_PREFIX}{device_id}/{revision}")
5120}
5121
5122pub fn ack_semantic_prefix(device_id: &str, revision: u64, ack_hash: ObjectHash) -> String {
5123 format!("{}/{ack_hash}", ack_slot_prefix(device_id, revision))
5124}
5125
5126pub fn snapshot_slot_prefix(device_id: &str, generation: u64) -> String {
5127 format!("{STORE_SNAPSHOT_META_PREFIX}{device_id}/{generation}")
5128}
5129
5130pub fn membership_entry_semantic_prefix(
5131 author: &str,
5132 author_owner_grant: &MembershipGrantId,
5133 stream_id: AuthorStreamId,
5134 seq: u64,
5135 entry_hash: ObjectHash,
5136) -> String {
5137 format!(
5138 "{STORE_MEMBERSHIP_ENTRY_PREFIX}{author}/{author_owner_grant}/{stream_id}/{seq}/{entry_hash}"
5139 )
5140}
5141
5142pub fn membership_head_semantic_prefix(
5143 author: &str,
5144 author_owner_grant: &MembershipGrantId,
5145 stream_id: AuthorStreamId,
5146 seq: u64,
5147 head_hash: ObjectHash,
5148) -> String {
5149 format!(
5150 "{STORE_MEMBERSHIP_HEAD_PREFIX}{author}/{author_owner_grant}/{stream_id}/{seq}/{head_hash}"
5151 )
5152}
5153
5154pub fn membership_head_slot_prefix(
5155 author: &str,
5156 author_owner_grant: &MembershipGrantId,
5157 stream_id: AuthorStreamId,
5158 seq: u64,
5159) -> String {
5160 format!("{STORE_MEMBERSHIP_HEAD_PREFIX}{author}/{author_owner_grant}/{stream_id}/{seq}")
5161}
5162
5163pub fn membership_resolution_semantic_prefix(
5164 conflict_hash: ObjectHash,
5165 resolver: &str,
5166 resolution_hash: ObjectHash,
5167) -> String {
5168 format!("store-v1/membership/resolutions/{conflict_hash}/{resolver}/{resolution_hash}")
5169}
5170
5171pub fn snapshot_image_semantic_prefix(author: &str, image_hash: ObjectHash) -> String {
5172 format!("{STORE_SNAPSHOT_IMAGE_PREFIX}{author}/{image_hash}")
5173}
5174
5175pub fn snapshot_semantic_prefix(author: &str, snapshot_hash: ObjectHash) -> String {
5176 format!("{STORE_SNAPSHOT_META_PREFIX}{author}/{snapshot_hash}")
5177}
5178
5179fn domain_json(domain: &[u8], value: &impl Serialize) -> Vec<u8> {
5180 let json = serde_json::to_vec(value).expect("canonical Store fields serialize");
5181 let mut bytes = Vec::with_capacity(domain.len() + json.len());
5182 bytes.extend_from_slice(domain);
5183 bytes.extend_from_slice(&json);
5184 bytes
5185}
5186
5187fn require_version(version: u32) -> Result<(), StoreProtocolError> {
5188 if version == STORE_PROTOCOL_VERSION {
5189 Ok(())
5190 } else {
5191 Err(StoreProtocolError::UnsupportedVersion(version))
5192 }
5193}
5194
5195fn validate_chained_revision(
5196 revision: u64,
5197 previous_hash: Option<ObjectHash>,
5198) -> Result<(), StoreProtocolError> {
5199 match (revision, previous_hash) {
5200 (0, _) => Err(StoreProtocolError::InvalidRevision(0)),
5201 (1, None) => Ok(()),
5202 (1, Some(_)) => Err(StoreProtocolError::UnexpectedControlPredecessor),
5203 (_, Some(_)) => Ok(()),
5204 (_, None) => Err(StoreProtocolError::MissingControlPredecessor),
5205 }
5206}
5207
5208fn validate_commit_order(order: &StoreCommitOrder) -> Result<(), StoreProtocolError> {
5209 let seq = order.seq();
5210 if seq == 0 {
5211 return Err(StoreProtocolError::InvalidSequence(0));
5212 }
5213 match order {
5214 StoreCommitOrder::MergeConcurrent {
5215 predecessor,
5216 dependencies,
5217 ..
5218 } => {
5219 match (seq, predecessor) {
5220 (1, None) => {}
5221 (1, Some(_)) => return Err(StoreProtocolError::UnexpectedPredecessor),
5222 (_, None) => return Err(StoreProtocolError::MissingPredecessor),
5223 (_, Some(reference)) => match reference.coord {
5224 StoreCommitCoord::MergeConcurrent { sequence, .. }
5225 if sequence.checked_add(1) == Some(seq) => {}
5226 _ => {
5227 return Err(StoreProtocolError::Malformed(
5228 "Merge predecessor is not the preceding Merge commit".to_string(),
5229 ));
5230 }
5231 },
5232 }
5233 for (stream_id, reference) in dependencies {
5234 match reference.coord {
5235 StoreCommitCoord::MergeConcurrent {
5236 stream_id: declared,
5237 sequence,
5238 } if declared == *stream_id && sequence > 0 => {}
5239 _ => {
5240 return Err(StoreProtocolError::Malformed(format!(
5241 "Merge dependency {stream_id} has a different exact coordinate"
5242 )));
5243 }
5244 }
5245 }
5246 }
5247 StoreCommitOrder::Serial { predecessor, .. } => match (seq, predecessor) {
5248 (1, StoreSerialPredecessor::Genesis { .. }) => {}
5249 (1, StoreSerialPredecessor::Commit(_)) => {
5250 return Err(StoreProtocolError::UnexpectedPredecessor);
5251 }
5252 (_, StoreSerialPredecessor::Genesis { .. }) => {
5253 return Err(StoreProtocolError::MissingPredecessor);
5254 }
5255 (_, StoreSerialPredecessor::Commit(reference)) => match reference.coord {
5256 StoreCommitCoord::Serial { sequence } if sequence.checked_add(1) == Some(seq) => {}
5257 _ => {
5258 return Err(StoreProtocolError::Malformed(
5259 "Serial predecessor is not the preceding Serial commit".to_string(),
5260 ));
5261 }
5262 },
5263 },
5264 }
5265 Ok(())
5266}
5267
5268fn validate_commit_predecessor_states(
5269 order: &StoreCommitOrder,
5270 membership: &StoreMembershipStateRef,
5271 devices: &StoreDeviceStateRef,
5272) -> Result<(), StoreProtocolError> {
5273 membership.validate_shape()?;
5274 if membership.write_policy() != order.policy() || devices.write_policy() != order.policy() {
5275 return Err(StoreProtocolError::WritePolicyMismatch {
5276 expected: order.policy(),
5277 actual: if membership.write_policy() != order.policy() {
5278 membership.write_policy()
5279 } else {
5280 devices.write_policy()
5281 },
5282 });
5283 }
5284 if membership.recovery() != devices.recovery() {
5285 return Err(StoreProtocolError::OwnerRecoveryMismatch);
5286 }
5287 validate_recovery_cursors(membership.recovery())?;
5288 validate_recovery_cursors(devices.recovery())?;
5289 match (order, membership, devices) {
5290 (
5291 StoreCommitOrder::MergeConcurrent {
5292 predecessor,
5293 dependencies,
5294 ..
5295 },
5296 StoreMembershipStateRef::MergeConcurrent { .. },
5297 StoreDeviceStateRef::MergeConcurrent { frontier, .. },
5298 ) => {
5299 let mut expected = dependencies.clone();
5300 if let Some(predecessor) = predecessor {
5301 let StoreCommitCoord::MergeConcurrent { stream_id, .. } = predecessor.coord else {
5302 return Err(StoreProtocolError::Malformed(
5303 "Merge predecessor has a Serial coordinate".to_string(),
5304 ));
5305 };
5306 if expected
5307 .insert(stream_id, predecessor.clone())
5308 .is_some_and(|dependency| dependency != *predecessor)
5309 {
5310 return Err(StoreProtocolError::Malformed(
5311 "Merge predecessor disagrees with the same-stream dependency".to_string(),
5312 ));
5313 }
5314 }
5315 if frontier != &CommitFrontier::MergeConcurrent(expected) {
5316 return Err(StoreProtocolError::Malformed(
5317 "Store device state names a different Merge predecessor cut".to_string(),
5318 ));
5319 }
5320 Ok(())
5321 }
5322 (
5323 StoreCommitOrder::Serial { predecessor, .. },
5324 StoreMembershipStateRef::Serial { position, .. },
5325 StoreDeviceStateRef::Serial {
5326 position: device_position,
5327 ..
5328 },
5329 ) => {
5330 if position != predecessor || device_position != predecessor {
5331 return Err(StoreProtocolError::Malformed(
5332 "Store predecessor state differs from the exact Serial predecessor".to_string(),
5333 ));
5334 }
5335 Ok(())
5336 }
5337 _ => Err(StoreProtocolError::WritePolicyMismatch {
5338 expected: order.policy(),
5339 actual: membership.write_policy(),
5340 }),
5341 }
5342}
5343
5344fn validate_commit_frontier(frontier: &CommitFrontier) -> Result<(), StoreProtocolError> {
5345 match frontier {
5346 CommitFrontier::MergeConcurrent(frontier) => {
5347 for (stream_id, reference) in frontier {
5348 match reference.coord {
5349 StoreCommitCoord::MergeConcurrent {
5350 stream_id: declared,
5351 sequence,
5352 } if declared == *stream_id && sequence > 0 => {}
5353 _ => {
5354 return Err(StoreProtocolError::Malformed(format!(
5355 "Merge frontier entry {stream_id} has a different exact coordinate"
5356 )));
5357 }
5358 }
5359 }
5360 Ok(())
5361 }
5362 CommitFrontier::Serial(Some(reference)) if !matches!(reference.coord, StoreCommitCoord::Serial { sequence } if sequence > 0) => {
5363 Err(StoreProtocolError::Malformed(
5364 "Serial frontier contains an invalid exact coordinate".to_string(),
5365 ))
5366 }
5367 CommitFrontier::Serial(_) => Ok(()),
5368 }
5369}
5370
5371fn validate_store_history_cut(frontier: &StoreHistoryCut) -> Result<(), StoreProtocolError> {
5372 match frontier {
5373 StoreHistoryCut::MergeConcurrent(commits) => {
5374 validate_commit_frontier(&CommitFrontier::MergeConcurrent(commits.clone()))
5375 }
5376 StoreHistoryCut::Serial(StoreSerialPredecessor::Genesis { .. }) => Ok(()),
5377 StoreHistoryCut::Serial(StoreSerialPredecessor::Commit(reference)) => {
5378 validate_commit_frontier(&CommitFrontier::Serial(Some(reference.clone())))
5379 }
5380 }
5381}
5382
5383fn validate_ack_history_cut(
5384 store_root_hash: ObjectHash,
5385 _author: &StoreDeviceRegistrationRef,
5386 cut: &StoreHistoryCut,
5387) -> Result<(), StoreProtocolError> {
5388 if let StoreHistoryCut::Serial(StoreSerialPredecessor::Genesis {
5389 root,
5390 founder_registration: _,
5391 }) = cut
5392 {
5393 if root.store_root_hash != store_root_hash {
5394 return Err(StoreProtocolError::Malformed(
5395 "Serial genesis acknowledgement belongs to another exact Store root".to_string(),
5396 ));
5397 }
5398 }
5399 Ok(())
5400}
5401
5402fn validate_membership_coord(coord: &MembershipCoord) -> Result<(), StoreProtocolError> {
5403 if coord.seq == 0 || coord.author_pubkey.is_empty() {
5404 return Err(StoreProtocolError::InvalidMembershipCoordinate {
5405 author: coord.author_pubkey.clone(),
5406 grant: coord.author_owner_grant.to_string(),
5407 stream_id: coord.stream_id.to_string(),
5408 seq: coord.seq,
5409 entry_hash: coord.entry_hash.to_string(),
5410 });
5411 }
5412 Ok(())
5413}
5414
5415fn validate_membership_authority(
5416 authority: &MembershipGrantCreationAuthority,
5417) -> Result<(), StoreProtocolError> {
5418 match authority {
5419 MembershipGrantCreationAuthority::Entry(coord) => validate_membership_coord(coord),
5420 MembershipGrantCreationAuthority::ConflictResolution(reference) => {
5421 let resolver = hex::decode(&reference.resolver_pubkey).map_err(|_| {
5422 StoreProtocolError::InvalidMembershipResolutionAuthority(
5423 reference.resolver_pubkey.clone(),
5424 )
5425 })?;
5426 if resolver.len() != crate::keys::SIGN_PUBLICKEYBYTES {
5427 return Err(StoreProtocolError::InvalidMembershipResolutionAuthority(
5428 reference.resolver_pubkey.clone(),
5429 ));
5430 }
5431 Ok(())
5432 }
5433 }
5434}
5435
5436#[cfg(test)]
5437mod tests {
5438 use super::*;
5439 use crate::sync::membership::founder_entry;
5440
5441 fn routing_hash() -> ObjectHash {
5442 ObjectHash::digest(b"test-sync-schema")
5443 }
5444
5445 struct Fixture {
5446 signer: UserKeypair,
5447 root: StoreProtocolRoot,
5448 root_ref: StoreRootRef,
5449 registration: StoreDeviceRegistration,
5450 registration_ref: StoreDeviceRegistrationRef,
5451 commit: StoreBatchCommit,
5452 commit_ref: StoreBatchCommitRef,
5453 package: Vec<u8>,
5454 }
5455
5456 fn slot(key: String) -> ObjectSlot {
5457 ObjectSlot::logical(key).expect("valid test object slot")
5458 }
5459
5460 fn exact(key: String, bytes: &[u8]) -> ExactObjectRef {
5461 ExactObjectRef::new(slot(key), bytes.len() as u64, ObjectHash::digest(bytes))
5462 }
5463
5464 fn fixture() -> Fixture {
5465 let signer = UserKeypair::generate();
5466 let founder_grant =
5467 crate::sync::test_helpers::test_membership_grant_id("store-a founder grant");
5468 let provider_admin =
5469 crate::sync::test_helpers::test_serial_founder_provider_admin("store-a");
5470 let founder_recovery = GrantStreamAnchor::OwnerRecovery {
5471 first_slot: slot("store-v1/recovery/founder/1.json".to_string()),
5472 };
5473 let store_protocol_root = StoreProtocolRoot::signed(
5474 StoreCreationDescriptor {
5475 version: STORE_PROTOCOL_VERSION,
5476 creation_id: StoreCreationId::from_nonce("store-a"),
5477 provider: crate::sync::storage::StoreProviderBinding::S3 {
5478 endpoint: crate::sync::storage::S3EndpointBinding::Custom {
5479 origin: "https://test.invalid".to_string(),
5480 },
5481 region: "test-region".to_string(),
5482 bucket: "store-a-bucket".to_string(),
5483 key_prefix: None,
5484 },
5485 schema_version: 3,
5486 sync_routing_hash: routing_hash(),
5487 write_policy: WritePolicy::Serial,
5488 founder_pubkey: keys::public_key_hex(&signer),
5489 founder_grant: founder_grant.clone(),
5490 root_slot: slot(format!("{}.json", store_protocol_root_logical_key())),
5491 founder_registration: slot(
5492 "store-v1/device-registrations/founder.json".to_string(),
5493 ),
5494 founder_provider_admin: provider_admin.clone(),
5495 membership: StoreMembershipGenesis::Serial,
5496 founder_recovery: founder_recovery.clone(),
5497 },
5498 &signer,
5499 )
5500 .expect("sign Store protocol root");
5501 let store_root_id = store_protocol_root.descriptor.store_root_id();
5502 let founder = founder_entry(
5503 &store_root_id.to_string(),
5504 &signer,
5505 founder_grant,
5506 "0000000001000-0000-device-a",
5507 GrantStreamAnchor::StoreMembership {
5508 first_slot: slot("store-v1/membership/founder/1.json".to_string()),
5509 },
5510 provider_admin,
5511 );
5512 let root_bytes = store_protocol_root.to_bytes();
5513 let root_ref = StoreRootRef {
5514 store_root_id,
5515 store_root_hash: store_protocol_root.object_hash(),
5516 object: exact(
5517 format!("{}.json", store_protocol_root_logical_key()),
5518 &root_bytes,
5519 ),
5520 };
5521 let registration = StoreDeviceRegistration::signed(
5522 root_ref.clone(),
5523 StoreDeviceRegistrationOrigin::Founder {
5524 creation_id: StoreCreationId::from_nonce("store-a"),
5525 },
5526 crate::sync::storage::ProviderDeviceBinding {
5527 principal: crate::sync::storage::ProviderPrincipalId::CustomS3Credential {
5528 access_key_id_hash: ObjectHash::digest(b"test access key"),
5529 },
5530 },
5531 StoreCommitAnchor::Serial,
5532 DeviceStreamAnchor::StoreAcknowledgements {
5533 first_slot: slot("store-v1/acks/founder/1.json".to_string()),
5534 },
5535 DeviceStreamAnchor::StoreSnapshots {
5536 first_slot: slot("store-v1/snapshots/founder/1.json".to_string()),
5537 },
5538 &signer,
5539 )
5540 .expect("sign founder registration");
5541 let registration_bytes = registration.to_bytes();
5542 let registration_ref = StoreDeviceRegistrationRef::from_registration(
5543 ®istration,
5544 exact(
5545 format!(
5546 "{}.json",
5547 registration_semantic_prefix(®istration.device_id.to_string())
5548 ),
5549 ®istration_bytes,
5550 ),
5551 );
5552 let predecessor = StoreSerialPredecessor::Genesis {
5553 root: root_ref.clone(),
5554 founder_registration: registration_ref.clone(),
5555 };
5556 let resolved_devices = ResolvedStoreDeviceState::founder(
5557 &root_ref,
5558 registration_ref.clone(),
5559 &store_protocol_root.descriptor.founder_pubkey,
5560 founder.author_owner_grant.clone(),
5561 &founder_recovery,
5562 )
5563 .expect("founder device state");
5564 let device_state = StoreDeviceStateRef::serial(predecessor.clone(), &resolved_devices)
5565 .expect("founder device state ref");
5566 let membership = crate::sync::membership::SerialMembershipState::from_founder(
5567 root_ref.store_root_id,
5568 &founder,
5569 )
5570 .expect("founder membership");
5571 let authorization =
5572 crate::sync::membership::SerialAuthorizationState::from_test_membership(
5573 &founder, membership,
5574 )
5575 .expect("founder authorization");
5576 let membership_state = StoreMembershipStateRef::serial(
5577 predecessor.clone(),
5578 resolved_devices.recovery.clone(),
5579 &authorization,
5580 )
5581 .expect("founder membership ref");
5582 let package = b"package".to_vec();
5583 let write_id = WriteId::from_generated("canonical-write".to_string());
5584 let order = StoreCommitOrder::Serial {
5585 seq: 1,
5586 predecessor,
5587 };
5588 let candidate_family = CandidateFamilyId::derive(
5589 root_ref.store_root_hash,
5590 ®istration_ref,
5591 &write_id,
5592 &order,
5593 );
5594 let package_object = exact(
5595 format!(
5596 "{}.pkg",
5597 package_semantic_prefix(
5598 candidate_family,
5599 SERIAL_STREAM_ID,
5600 1,
5601 ObjectHash::digest(&package),
5602 )
5603 ),
5604 &package,
5605 );
5606 let device_signer = registration.device_signer(&signer).unwrap();
5607 let commit = StoreBatchCommit::signed(
5608 root_ref.store_root_hash,
5609 write_id,
5610 StoreCommitCoord::Serial { sequence: 1 },
5611 registration_ref.clone(),
5612 ®istration,
5613 order,
5614 membership_state,
5615 device_state,
5616 None,
5617 StorePackageInput {
5618 candidate_family,
5619 schema_version: 3,
5620 bytes: &package,
5621 object: package_object,
5622 },
5623 &device_signer,
5624 )
5625 .expect("sign commit");
5626 let commit_bytes = commit.to_bytes();
5627 let commit_ref = StoreBatchCommitRef::from_commit(
5628 &commit,
5629 StoreCommitCoord::Serial { sequence: 1 },
5630 exact(
5631 format!(
5632 "{}.json",
5633 commit_semantic_prefix(
5634 commit.candidate_family(),
5635 SERIAL_STREAM_ID,
5636 1,
5637 commit.commit_hash(),
5638 )
5639 ),
5640 &commit_bytes,
5641 ),
5642 )
5643 .expect("exact commit ref");
5644 Fixture {
5645 signer,
5646 root: store_protocol_root,
5647 root_ref,
5648 registration,
5649 registration_ref,
5650 commit,
5651 commit_ref,
5652 package,
5653 }
5654 }
5655
5656 #[test]
5657 fn object_hash_is_strict_lowercase_hex() {
5658 let hash = ObjectHash::digest(b"fixture");
5659 assert_eq!(hash.to_string().parse::<ObjectHash>().unwrap(), hash);
5660 assert!(hash
5661 .to_string()
5662 .to_uppercase()
5663 .parse::<ObjectHash>()
5664 .is_err());
5665 assert!("0".repeat(63).parse::<ObjectHash>().is_err());
5666 assert!(format!("{}g", "0".repeat(63))
5667 .parse::<ObjectHash>()
5668 .is_err());
5669 }
5670
5671 #[test]
5672 fn canonical_commit_round_trip_and_literal_bytes() {
5673 let fixture = fixture();
5674 let bytes = fixture.commit.to_bytes();
5675 let parsed = StoreBatchCommit::parse_at(
5676 &bytes,
5677 fixture.root_ref.store_root_hash,
5678 &fixture.commit_ref.coord,
5679 &fixture.registration,
5680 )
5681 .expect("parse commit");
5682 parsed
5683 .verify_store_package(&fixture.package)
5684 .expect("verify package");
5685 assert_eq!(parsed, fixture.commit);
5686 assert!(fixture
5687 .commit
5688 .canonical_signed_bytes()
5689 .starts_with(COMMIT_DOMAIN));
5690 }
5691
5692 #[test]
5693 fn commit_rejects_package_signature_and_coordinate_tamper() {
5694 let fixture = fixture();
5695 let mut tampered = fixture.commit.clone();
5696 tampered.signature.push('0');
5697 assert!(matches!(
5698 tampered.verify_at(
5699 fixture.root_ref.store_root_hash,
5700 &fixture.commit_ref.coord,
5701 &fixture.registration,
5702 ),
5703 Err(StoreProtocolError::InvalidSignature)
5704 ));
5705
5706 let mut tampered = fixture.commit.clone();
5707 let StoreCommitBody::Operations(operations) = &mut tampered.body else {
5708 panic!("fixture commit carries operations")
5709 };
5710 operations
5711 .store_package
5712 .as_mut()
5713 .expect("fixture has Store package")
5714 .content_hash = ObjectHash::digest(b"different");
5715 assert!(matches!(
5716 tampered.verify_at(
5717 fixture.root_ref.store_root_hash,
5718 &fixture.commit_ref.coord,
5719 &fixture.registration,
5720 ),
5721 Err(StoreProtocolError::RelocatedPackage { .. })
5722 ));
5723
5724 assert!(matches!(
5725 fixture.commit.verify_at(
5726 fixture.root_ref.store_root_hash,
5727 &StoreCommitCoord::Serial { sequence: 2 },
5728 &fixture.registration,
5729 ),
5730 Err(StoreProtocolError::RelocatedSlot { .. })
5731 ));
5732 assert!(matches!(
5733 fixture.commit.verify_store_package(b"different"),
5734 Err(StoreProtocolError::PackageLengthMismatch { .. })
5735 | Err(StoreProtocolError::PackageHashMismatch { .. })
5736 ));
5737 fixture
5738 .commit
5739 .verify_store_package(&fixture.package)
5740 .unwrap();
5741 }
5742
5743 #[test]
5744 fn unknown_fields_and_versions_are_rejected() {
5745 let fixture = fixture();
5746 let mut value = serde_json::to_value(&fixture.commit).unwrap();
5747 value["unknown"] = serde_json::json!(true);
5748 assert!(StoreBatchCommit::parse_at(
5749 &serde_json::to_vec(&value).unwrap(),
5750 fixture.root_ref.store_root_hash,
5751 &fixture.commit_ref.coord,
5752 &fixture.registration,
5753 )
5754 .is_err());
5755
5756 let mut value = serde_json::to_value(&fixture.commit).unwrap();
5757 value["version"] = serde_json::json!(2);
5758 assert!(matches!(
5759 StoreBatchCommit::parse_at(
5760 &serde_json::to_vec(&value).unwrap(),
5761 fixture.root_ref.store_root_hash,
5762 &fixture.commit_ref.coord,
5763 &fixture.registration,
5764 ),
5765 Err(StoreProtocolError::UnsupportedVersion(2))
5766 ));
5767 }
5768
5769 #[test]
5770 fn readiness_rejects_a_bootstrap_cut_other_than_the_signed_attempt_cut() {
5771 let fixture = fixture();
5772 let joiner = UserKeypair::generate();
5773 let attempt_id = DeviceJoinAttemptId::from_hash(ObjectHash::digest(b"join attempt"));
5774 let attempt_slot = slot("store-v1/device-join-attempts/test.json".to_string());
5775 let outcome_slot = slot("store-v1/device-join-outcomes/test.json".to_string());
5776 let registration_slot = slot("store-v1/device-registrations/joiner.json".to_string());
5777 let provider_admin = crate::sync::provider::ProviderAdminState::founder_from_root(
5778 fixture.root_ref.clone(),
5779 fixture.registration_ref.clone(),
5780 &fixture.root.descriptor.founder_provider_admin,
5781 )
5782 .records()
5783 .values()
5784 .next()
5785 .expect("founder provider administrator exists")
5786 .clone();
5787 let registration = StoreDeviceRegistration::signed(
5788 fixture.root_ref.clone(),
5789 StoreDeviceRegistrationOrigin::Join {
5790 attempt_id,
5791 attempt_slot: attempt_slot.clone(),
5792 outcome_slot: outcome_slot.clone(),
5793 },
5794 provider_admin.provider.clone(),
5795 StoreCommitAnchor::MergeConcurrent {
5796 announcements: DeviceStreamAnchor::StoreAnnouncements {
5797 first_slot: slot("store-v1/heads/joiner/1.json".to_string()),
5798 },
5799 },
5800 DeviceStreamAnchor::StoreAcknowledgements {
5801 first_slot: slot("store-v1/acks/joiner/1.json".to_string()),
5802 },
5803 DeviceStreamAnchor::StoreSnapshots {
5804 first_slot: slot("store-v1/snapshots/joiner/1.json".to_string()),
5805 },
5806 &joiner,
5807 )
5808 .unwrap();
5809 let registration_ref = StoreDeviceRegistrationRef::from_registration(
5810 ®istration,
5811 ExactObjectRef::new(
5812 registration_slot.clone(),
5813 registration.to_bytes().len() as u64,
5814 ObjectHash::digest(®istration.to_bytes()),
5815 ),
5816 );
5817 let membership = StoreMembershipStateRef::merge_concurrent(
5818 Vec::new(),
5819 Vec::new(),
5820 Vec::new(),
5821 ObjectHash::digest(b"membership"),
5822 )
5823 .unwrap();
5824 let attempt_cut = StoreHistoryCut::MergeConcurrent(BTreeMap::new());
5825 let owner_device_signer = fixture.registration.device_signer(&fixture.signer).unwrap();
5826 let offer = crate::sync::device_join::DeviceJoinOffer::signed(
5827 attempt_id,
5828 keys::public_key_hex(&joiner),
5829 fixture.root_ref.clone(),
5830 fixture.root.descriptor.provider.clone(),
5831 attempt_slot.clone(),
5832 outcome_slot.clone(),
5833 fixture.registration_ref.clone(),
5834 fixture.root.descriptor.founder_grant.clone(),
5835 provider_admin.clone(),
5836 &fixture.registration,
5837 &owner_device_signer,
5838 )
5839 .unwrap();
5840 let access_request = crate::sync::device_join::DeviceProviderAccessRequest::signed(
5841 offer,
5842 provider_admin.provider.clone(),
5843 &joiner,
5844 )
5845 .unwrap();
5846 let access_grant_id = crate::sync::provider::ProviderAccessGrantId::from_random_bytes(
5847 *ObjectHash::digest(b"join provider access grant").as_bytes(),
5848 );
5849 let access_grant = crate::sync::provider::StoreMemberProviderAccessGrant::signed(
5850 access_grant_id,
5851 keys::public_key_hex(&joiner),
5852 provider_admin.provider.clone(),
5853 provider_admin.access.clone(),
5854 provider_admin.grant_id.clone(),
5855 fixture.registration_ref.clone(),
5856 &fixture.root.descriptor.provider,
5857 &fixture.registration,
5858 &owner_device_signer,
5859 )
5860 .unwrap();
5861 let access_grant_ref = crate::sync::provider::StoreMemberProviderAccessGrantRef::from_grant(
5862 &access_grant,
5863 exact(
5864 provider_access_grant_semantic_prefix(&access_grant.grant_id) + ".json",
5865 &access_grant.to_bytes(),
5866 ),
5867 );
5868 let approval = crate::sync::device_join::DeviceProviderAdmissionApproval::signed(
5869 access_request,
5870 crate::sync::provider::ActivatedStoreMemberProviderAccessGrant {
5871 grant: access_grant,
5872 grant_ref: access_grant_ref,
5873 activation: fixture.commit_ref.clone(),
5874 },
5875 crate::sync::device_join::DeviceProviderAdmissionChallenge::SamePrincipal,
5876 &fixture.registration,
5877 &owner_device_signer,
5878 )
5879 .unwrap();
5880 let attempt = DeviceJoinAttempt::signed(
5881 fixture.root_ref.clone(),
5882 attempt_id,
5883 attempt_slot.clone(),
5884 registration.clone(),
5885 registration_slot,
5886 outcome_slot,
5887 attempt_cut,
5888 membership,
5889 provider_admin.grant_id,
5890 approval,
5891 crate::sync::device_join::DeviceProviderResponseReservation::SamePrincipal,
5892 fixture.registration_ref.clone(),
5893 fixture.root.descriptor.founder_grant.clone(),
5894 &fixture.registration,
5895 &owner_device_signer,
5896 )
5897 .unwrap();
5898 let attempt_ref = DeviceJoinAttemptRef {
5899 attempt_id,
5900 attempt_hash: attempt.attempt_hash(),
5901 object: ExactObjectRef::new(
5902 attempt_slot,
5903 attempt.to_bytes().len() as u64,
5904 ObjectHash::digest(&attempt.to_bytes()),
5905 ),
5906 };
5907 let stream_id = AuthorStreamId::from_digest(ObjectHash::digest(b"other stream"));
5908 let other_commit_hash = ObjectHash::digest(b"other commit");
5909 let other_commit = StoreBatchCommitRef {
5910 coord: StoreCommitCoord::MergeConcurrent {
5911 stream_id,
5912 sequence: 1,
5913 },
5914 commit_hash: other_commit_hash,
5915 object: exact(
5916 format!(
5917 "{}.json",
5918 commit_semantic_prefix(
5919 CandidateFamilyId::from_hash(ObjectHash::digest(
5920 b"other commit candidate family",
5921 )),
5922 &stream_id.to_string(),
5923 1,
5924 other_commit_hash,
5925 )
5926 ),
5927 b"other commit",
5928 ),
5929 };
5930 let other_cut =
5931 StoreHistoryCut::MergeConcurrent(BTreeMap::from([(stream_id, other_commit)]));
5932 let device_signer = registration.device_signer(&joiner).unwrap();
5933 let ack = StoreAck::signed(
5934 fixture.root_ref.store_root_hash,
5935 registration_ref.clone(),
5936 1,
5937 None,
5938 other_cut.clone(),
5939 "2026-07-16T00:00:00Z".to_string(),
5940 SuccessorLink {
5941 activation: StreamActivationId::store_acknowledgements(
5942 &fixture.root_ref,
5943 ®istration_ref,
5944 ),
5945 predecessor: None,
5946 next_slot: slot("store-v1/acks/joiner/2.json".to_string()),
5947 },
5948 &device_signer,
5949 )
5950 .unwrap();
5951 let ack_ref = StoreAckRef {
5952 revision: 1,
5953 ack_hash: ack.ack_hash(),
5954 object: exact("store-v1/acks/joiner/1.json".to_string(), &ack.to_bytes()),
5955 };
5956 let proof = DeviceReadinessProof::signed(
5957 attempt_ref.clone(),
5958 registration_ref,
5959 ack_ref.clone(),
5960 other_cut,
5961 ®istration,
5962 &device_signer,
5963 )
5964 .unwrap();
5965
5966 assert!(matches!(
5967 proof.verify(&attempt_ref, &attempt, ®istration, &ack_ref, &ack),
5968 Err(StoreProtocolError::DeviceReadinessMismatch)
5969 ));
5970 }
5971
5972 #[test]
5973 fn store_ack_semantic_hash_is_distinct_from_its_stored_json_hash() {
5974 let signer = UserKeypair::generate();
5975 let store_root_hash = ObjectHash::digest(b"ack semantic hash Store root");
5976 let root_ref = StoreRootRef {
5977 store_root_id: ObjectHash::digest(b"ack semantic hash Store id"),
5978 store_root_hash,
5979 object: exact(store_protocol_root_logical_key().to_string(), b"Store root"),
5980 };
5981 let origin = StoreDeviceRegistrationOrigin::Founder {
5982 creation_id: StoreCreationId::from_nonce("ack semantic hash founder"),
5983 };
5984 let registration_ref = StoreDeviceRegistrationRef {
5985 device_id: StoreDeviceId::derive(&root_ref, &origin),
5986 registration_hash: ObjectHash::digest(b"ack semantic hash registration"),
5987 object: exact(
5988 "store-v1/device-registrations/founder.json".to_string(),
5989 b"registration",
5990 ),
5991 };
5992 let ack = StoreAck::signed(
5993 store_root_hash,
5994 registration_ref.clone(),
5995 1,
5996 None,
5997 StoreHistoryCut::Serial(StoreSerialPredecessor::Genesis {
5998 root: root_ref.clone(),
5999 founder_registration: registration_ref.clone(),
6000 }),
6001 "2026-07-16T00:00:00Z".to_string(),
6002 SuccessorLink {
6003 activation: StreamActivationId::store_acknowledgements(
6004 &root_ref,
6005 ®istration_ref,
6006 ),
6007 predecessor: None,
6008 next_slot: slot("store-v1/acks/founder/2.json".to_string()),
6009 },
6010 &signer,
6011 )
6012 .unwrap();
6013 let bytes = ack.to_bytes();
6014 let semantic_hash = StoreAck::semantic_hash_from_bytes(&bytes).unwrap();
6015
6016 assert_eq!(semantic_hash, ack.ack_hash());
6017 assert_ne!(semantic_hash, ObjectHash::digest(&bytes));
6018 }
6019
6020 #[test]
6021 fn store_protocol_root_authenticates_the_creation_descriptor() {
6022 let fixture = fixture();
6023 let bytes = fixture.root.to_bytes();
6024 let parsed = StoreProtocolRoot::parse_expected(
6025 &bytes,
6026 &fixture.root_ref,
6027 WritePolicy::Serial,
6028 routing_hash(),
6029 )
6030 .expect("parse exact Store protocol root");
6031 assert_eq!(parsed, fixture.root);
6032 }
6033
6034 #[test]
6035 fn store_protocol_root_signs_the_required_write_policy() {
6036 let fixture = fixture();
6037 let value = serde_json::to_value(fixture.root).expect("serialize Store root");
6038
6039 let descriptor = value
6040 .get("descriptor")
6041 .expect("Store root carries its creation descriptor");
6042 assert_eq!(
6043 descriptor.get("write_policy"),
6044 Some(&serde_json::json!("serial"))
6045 );
6046 assert!(
6047 descriptor.get("sync_routing_hash").is_some(),
6048 "the signed Store root must bind the sync-routing contract"
6049 );
6050 }
6051
6052 #[test]
6053 fn operations_commit_uses_the_closed_body_and_signed_manifest_shape() {
6054 let fixture = fixture();
6055 let value = serde_json::to_value(&fixture.commit).expect("serialize Store commit");
6056
6057 assert!(value.get("package").is_none());
6058 assert!(value.get("store_package").is_none());
6059 assert!(value.get("device_registrations").is_none());
6060 assert!(value.get("device_retirements").is_none());
6061 assert!(value.get("circle_controls").is_none());
6062 assert!(value.get("circle_packages").is_none());
6063 let operations = value
6064 .get("body")
6065 .and_then(|body| body.get("operations"))
6066 .expect("Store commit carries one closed operations body");
6067 assert!(operations.get("store_package").is_some());
6068 assert_eq!(
6069 operations.get("device_registrations"),
6070 Some(&serde_json::json!([]))
6071 );
6072 assert_eq!(
6073 operations.get("circle_controls"),
6074 Some(&serde_json::json!([]))
6075 );
6076 assert_eq!(
6077 operations.get("circle_packages"),
6078 Some(&serde_json::json!([]))
6079 );
6080 assert_eq!(
6081 value
6082 .get("candidate_objects")
6083 .and_then(|manifest| manifest.get("family")),
6084 Some(&serde_json::to_value(fixture.commit.candidate_family()).unwrap())
6085 );
6086 }
6087
6088 fn resign_commit(commit: &mut StoreBatchCommit, fixture: &Fixture) {
6089 let signer = fixture.registration.device_signer(&fixture.signer).unwrap();
6090 commit.signature = keys::sign_hex(&signer, &commit.canonical_signed_bytes()).1;
6091 }
6092
6093 fn candidate_cleanup_manifest(fixture: &Fixture, label: &str) -> CandidateCleanupManifest {
6094 let package = label.as_bytes();
6095 let write_id = WriteId::from_generated(format!("{label}-write"));
6096 let order = fixture.commit.order.clone();
6097 let family = CandidateFamilyId::derive(
6098 fixture.root_ref.store_root_hash,
6099 &fixture.registration_ref,
6100 &write_id,
6101 &order,
6102 );
6103 let package_object = exact(
6104 format!(
6105 "{}.pkg",
6106 package_semantic_prefix(
6107 family,
6108 SERIAL_STREAM_ID,
6109 order.seq(),
6110 ObjectHash::digest(package),
6111 )
6112 ),
6113 package,
6114 );
6115 let signer = fixture.registration.device_signer(&fixture.signer).unwrap();
6116 let commit = StoreBatchCommit::signed(
6117 fixture.root_ref.store_root_hash,
6118 write_id,
6119 fixture.commit_ref.coord.clone(),
6120 fixture.registration_ref.clone(),
6121 &fixture.registration,
6122 order,
6123 fixture.commit.membership_state.clone(),
6124 fixture.commit.device_state.clone(),
6125 None,
6126 StorePackageInput {
6127 candidate_family: family,
6128 schema_version: 3,
6129 bytes: package,
6130 object: package_object,
6131 },
6132 &signer,
6133 )
6134 .expect("sign candidate commit");
6135 let bytes = commit.to_bytes();
6136 CandidateCleanupManifest {
6137 candidate: StoreBatchCommitDeletionTarget {
6138 coord: fixture.commit_ref.coord.clone(),
6139 object: exact(
6140 format!(
6141 "{}.json",
6142 commit_semantic_prefix(
6143 commit.candidate_family(),
6144 SERIAL_STREAM_ID,
6145 commit.seq(),
6146 commit.commit_hash(),
6147 )
6148 ),
6149 &bytes,
6150 ),
6151 canonical_signed_bytes: bytes,
6152 },
6153 }
6154 }
6155
6156 fn sign_candidate_abandonment(
6157 fixture: &Fixture,
6158 manifests: Vec<CandidateCleanupManifest>,
6159 ) -> Result<StoreBatchCommit, StoreProtocolError> {
6160 let signer = fixture.registration.device_signer(&fixture.signer).unwrap();
6161 StoreBatchCommit::signed_with_candidate_abandonment(
6162 fixture.root_ref.store_root_hash,
6163 WriteId::from_generated("abandon-candidates".to_string()),
6164 fixture.commit_ref.coord.clone(),
6165 fixture.registration_ref.clone(),
6166 &fixture.registration,
6167 fixture.commit.order.clone(),
6168 fixture.commit.membership_state.clone(),
6169 fixture.commit.device_state.clone(),
6170 manifests,
6171 &signer,
6172 )
6173 }
6174
6175 #[test]
6176 fn candidate_abandonment_is_signed_canonical_cleanup_authority() {
6177 let fixture = fixture();
6178 let first = candidate_cleanup_manifest(&fixture, "first candidate");
6179 let second = candidate_cleanup_manifest(&fixture, "second candidate");
6180 let commit = sign_candidate_abandonment(&fixture, vec![second.clone(), first.clone()])
6181 .expect("sign candidate abandonment");
6182
6183 assert!(commit.candidate_objects.objects.is_empty());
6184 assert_eq!(
6185 commit.abandoned_candidates(),
6186 [first, second]
6187 .into_iter()
6188 .collect::<BTreeSet<_>>()
6189 .into_iter()
6190 .collect::<Vec<_>>()
6191 );
6192 commit
6193 .verify_at(
6194 fixture.root_ref.store_root_hash,
6195 &fixture.commit_ref.coord,
6196 &fixture.registration,
6197 )
6198 .expect("verify candidate abandonment");
6199 }
6200
6201 #[test]
6202 fn candidate_abandonment_rejects_duplicate_and_inexact_targets() {
6203 let fixture = fixture();
6204 let manifest = candidate_cleanup_manifest(&fixture, "candidate");
6205 assert!(matches!(
6206 sign_candidate_abandonment(&fixture, vec![manifest.clone(), manifest.clone()]),
6207 Err(StoreProtocolError::Malformed(reason))
6208 if reason.contains("strictly sorted and unique")
6209 ));
6210
6211 let mut inexact = manifest;
6212 inexact.candidate.object = exact(
6213 inexact.candidate.object.slot().logical_key().to_string(),
6214 b"different stored bytes",
6215 );
6216 assert!(matches!(
6217 sign_candidate_abandonment(&fixture, vec![inexact]),
6218 Err(StoreProtocolError::Malformed(reason))
6219 if reason.contains("does not match stored size/hash")
6220 ));
6221 }
6222
6223 #[test]
6224 fn candidate_abandonment_rejects_noncanonical_or_unsigned_candidate_bytes() {
6225 let fixture = fixture();
6226 let manifest = candidate_cleanup_manifest(&fixture, "candidate");
6227 let candidate: StoreBatchCommit =
6228 serde_json::from_slice(&manifest.candidate.canonical_signed_bytes).unwrap();
6229
6230 let mut noncanonical = manifest.clone();
6231 noncanonical.candidate.canonical_signed_bytes =
6232 serde_json::to_vec_pretty(&candidate).expect("serialize noncanonical candidate");
6233 noncanonical.candidate.object = exact(
6234 noncanonical
6235 .candidate
6236 .object
6237 .slot()
6238 .logical_key()
6239 .to_string(),
6240 &noncanonical.candidate.canonical_signed_bytes,
6241 );
6242 assert!(matches!(
6243 sign_candidate_abandonment(&fixture, vec![noncanonical]),
6244 Err(StoreProtocolError::Malformed(reason))
6245 if reason.contains("not canonical")
6246 ));
6247
6248 let mut unsigned_candidate = candidate;
6249 unsigned_candidate.signature.push('0');
6250 let mut unsigned = manifest;
6251 unsigned.candidate.canonical_signed_bytes = unsigned_candidate.to_bytes();
6252 unsigned.candidate.object = exact(
6253 unsigned.candidate.object.slot().logical_key().to_string(),
6254 &unsigned.candidate.canonical_signed_bytes,
6255 );
6256 assert!(matches!(
6257 sign_candidate_abandonment(&fixture, vec![unsigned]),
6258 Err(StoreProtocolError::InvalidSignature)
6259 ));
6260 }
6261
6262 #[test]
6263 fn candidate_abandonment_rejects_retained_authority_target() {
6264 let fixture = fixture();
6265 let inner = sign_candidate_abandonment(
6266 &fixture,
6267 vec![candidate_cleanup_manifest(&fixture, "candidate")],
6268 )
6269 .expect("sign inner candidate abandonment");
6270 let bytes = inner.to_bytes();
6271 let retained = CandidateCleanupManifest {
6272 candidate: StoreBatchCommitDeletionTarget {
6273 coord: fixture.commit_ref.coord.clone(),
6274 object: exact(
6275 format!(
6276 "{}.json",
6277 commit_semantic_prefix(
6278 inner.candidate_family(),
6279 SERIAL_STREAM_ID,
6280 inner.seq(),
6281 inner.commit_hash(),
6282 )
6283 ),
6284 &bytes,
6285 ),
6286 canonical_signed_bytes: bytes,
6287 },
6288 };
6289
6290 assert!(matches!(
6291 sign_candidate_abandonment(&fixture, vec![retained]),
6292 Err(StoreProtocolError::Malformed(reason))
6293 if reason.contains("retained authority")
6294 ));
6295 }
6296
6297 #[test]
6298 fn parsed_candidate_abandonment_rejects_noncanonical_manifest_order() {
6299 let fixture = fixture();
6300 let first = candidate_cleanup_manifest(&fixture, "first candidate");
6301 let second = candidate_cleanup_manifest(&fixture, "second candidate");
6302 let mut commit = sign_candidate_abandonment(&fixture, vec![first, second])
6303 .expect("sign candidate abandonment");
6304 let StoreCommitBody::AbandonCandidates { manifests } = &mut commit.body else {
6305 panic!("commit carries candidate abandonment")
6306 };
6307 manifests.reverse();
6308 resign_commit(&mut commit, &fixture);
6309
6310 assert!(matches!(
6311 commit.verify_at(
6312 fixture.root_ref.store_root_hash,
6313 &fixture.commit_ref.coord,
6314 &fixture.registration,
6315 ),
6316 Err(StoreProtocolError::Malformed(reason))
6317 if reason.contains("strictly sorted and unique")
6318 ));
6319 }
6320
6321 #[test]
6322 fn commit_rejects_manifest_omission_invention_and_family_substitution() {
6323 let fixture = fixture();
6324
6325 let mut omitted = fixture.commit.clone();
6326 omitted.candidate_objects.objects.clear();
6327 resign_commit(&mut omitted, &fixture);
6328 assert!(matches!(
6329 omitted.verify_at(
6330 fixture.root_ref.store_root_hash,
6331 &fixture.commit_ref.coord,
6332 &fixture.registration,
6333 ),
6334 Err(StoreProtocolError::Malformed(reason))
6335 if reason.contains("manifest differs")
6336 ));
6337
6338 let mut invented = fixture.commit.clone();
6339 invented
6340 .candidate_objects
6341 .objects
6342 .push(invented.candidate_objects.objects[0].clone());
6343 resign_commit(&mut invented, &fixture);
6344 assert!(matches!(
6345 invented.verify_at(
6346 fixture.root_ref.store_root_hash,
6347 &fixture.commit_ref.coord,
6348 &fixture.registration,
6349 ),
6350 Err(StoreProtocolError::Malformed(reason))
6351 if reason.contains("manifest differs")
6352 ));
6353
6354 let mut substituted = fixture.commit.clone();
6355 substituted.candidate_objects.family =
6356 CandidateFamilyId::from_hash(ObjectHash::digest(b"substituted candidate family"));
6357 resign_commit(&mut substituted, &fixture);
6358 assert!(matches!(
6359 substituted.verify_at(
6360 fixture.root_ref.store_root_hash,
6361 &fixture.commit_ref.coord,
6362 &fixture.registration,
6363 ),
6364 Err(StoreProtocolError::Malformed(reason))
6365 if reason.contains("manifest differs")
6366 ));
6367 }
6368
6369 #[test]
6370 fn candidate_manifest_rejects_one_exact_object_reached_twice() {
6371 let fixture = fixture();
6372 let mut operations = fixture
6373 .commit
6374 .operations()
6375 .expect("fixture commit carries operations")
6376 .clone();
6377 let package = operations
6378 .store_package
6379 .clone()
6380 .expect("fixture commit carries a Store package");
6381 operations.circle_packages.push(CirclePackageRef {
6382 circle_id: CircleId::from_bytes([8; 16]),
6383 control: CircleControlCoord::Serial {
6384 author_pubkey: keys::public_key_hex(&fixture.signer),
6385 generation: 1,
6386 control_hash: ObjectHash::digest(b"duplicate exact object control"),
6387 },
6388 package,
6389 key_fingerprint: KeyFingerprint::from_bytes([9; 8]),
6390 });
6391
6392 assert!(matches!(
6393 candidate_manifest(
6394 fixture.commit.candidate_family(),
6395 &StoreCommitBody::Operations(operations),
6396 ),
6397 Err(StoreProtocolError::Malformed(reason))
6398 if reason.contains("repeats an exact object reference")
6399 ));
6400 }
6401
6402 #[test]
6403 fn candidate_manifest_rejects_duplicate_circle_access_with_distinct_provider_ids() {
6404 let fixture = fixture();
6405 let family = fixture.commit.candidate_family();
6406 let circle_id = CircleId::from_bytes([7; 16]);
6407 let owner_pubkey = keys::public_key_hex(&fixture.signer);
6408 let recipient_slot = "recipient-slot".to_string();
6409 let ids = crate::id_provider::SequentialIdProvider::new("duplicate Circle access");
6410 let epoch_id = CircleEpochId::generate(&ids);
6411 let leaf_id = AccessLeafId::generate(&ids);
6412 let leaf_hash = ObjectHash::digest(b"sealed access leaf");
6413 let control_hash = ObjectHash::digest(b"Circle access control");
6414 let leaf_key = circle_access_leaf_semantic_prefix(
6415 circle_id,
6416 family,
6417 &owner_pubkey,
6418 epoch_id,
6419 &recipient_slot,
6420 leaf_id,
6421 );
6422 let envelope_key = format!(
6423 "{}.json",
6424 circle_access_envelope_semantic_prefix(
6425 circle_id,
6426 family,
6427 &owner_pubkey,
6428 &recipient_slot,
6429 control_hash,
6430 )
6431 );
6432 let access = |provider_id: &str| CircleAccessObjectRef {
6433 leaf: CircleAccessLeafObjectRef {
6434 owner_pubkey: owner_pubkey.clone(),
6435 epoch_id,
6436 recipient_slot: recipient_slot.clone(),
6437 leaf_id,
6438 leaf_hash,
6439 object: ExactObjectRef::new(
6440 ObjectSlot::opaque(leaf_key.clone(), format!("{provider_id}-leaf")).unwrap(),
6441 18,
6442 leaf_hash,
6443 ),
6444 },
6445 envelope: CircleAccessEnvelopeObjectRef {
6446 owner_pubkey: owner_pubkey.clone(),
6447 recipient_slot: recipient_slot.clone(),
6448 control_hash,
6449 leaf_id,
6450 leaf_hash,
6451 object: ExactObjectRef::new(
6452 ObjectSlot::opaque(envelope_key.clone(), format!("{provider_id}-envelope"))
6453 .unwrap(),
6454 20,
6455 ObjectHash::digest(provider_id.as_bytes()),
6456 ),
6457 },
6458 };
6459 let control = CircleControlCoord::Serial {
6460 author_pubkey: owner_pubkey.clone(),
6461 generation: 1,
6462 control_hash,
6463 };
6464 let mut operations = fixture
6465 .commit
6466 .operations()
6467 .expect("fixture commit carries operations")
6468 .clone();
6469 operations.circle_controls.push(CircleControlRef::Serial {
6470 circle_id,
6471 control,
6472 objects: CircleActivationObjects {
6473 control: exact("circle-control.json".to_string(), b"control"),
6474 control_head: None,
6475 roster_entries: BTreeMap::new(),
6476 roster_heads: BTreeMap::new(),
6477 roster_resolutions: BTreeMap::new(),
6478 metadata_entries: BTreeMap::new(),
6479 metadata_heads: BTreeMap::new(),
6480 access: vec![access("drive-file-a"), access("drive-file-b")],
6481 },
6482 });
6483
6484 assert!(matches!(
6485 candidate_manifest(family, &StoreCommitBody::Operations(operations)),
6486 Err(StoreProtocolError::Malformed(reason))
6487 if reason.contains("repeats a Circle access semantic key")
6488 ));
6489 }
6490
6491 #[test]
6492 fn commit_reference_constructor_rejects_relocated_exact_object() {
6493 let fixture = fixture();
6494 let bytes = fixture.commit.to_bytes();
6495 let relocated = exact(
6496 format!(
6497 "store-v1/candidates/{}/packages/relocated.json",
6498 fixture.commit.candidate_family().as_hash()
6499 ),
6500 &bytes,
6501 );
6502
6503 assert!(matches!(
6504 StoreBatchCommitRef::from_commit(
6505 &fixture.commit,
6506 fixture.commit_ref.coord.clone(),
6507 relocated,
6508 ),
6509 Err(StoreProtocolError::RelocatedSlot { .. })
6510 ));
6511 }
6512
6513 #[test]
6514 fn self_retirement_is_exact_commit_state_and_dominates_active_merge_state() {
6515 let fixture = fixture();
6516 let write_id = WriteId::from_generated("retire-founder".to_string());
6517 let order = fixture.commit.order.clone();
6518 let candidate_family = CandidateFamilyId::derive(
6519 fixture.root_ref.store_root_hash,
6520 &fixture.registration_ref,
6521 &write_id,
6522 &order,
6523 );
6524 let retiring_cut = match &order {
6525 StoreCommitOrder::Serial { predecessor, .. } => {
6526 StoreHistoryCut::Serial(predecessor.clone())
6527 }
6528 StoreCommitOrder::MergeConcurrent { .. } => unreachable!(),
6529 };
6530 let device_signer = fixture.registration.device_signer(&fixture.signer).unwrap();
6531 let retirement = StoreDeviceSelfRetirement::signed(
6532 fixture.root_ref.store_root_hash,
6533 candidate_family,
6534 fixture.registration_ref.clone(),
6535 retiring_cut,
6536 &device_signer,
6537 )
6538 .unwrap();
6539 let retirement_bytes = retirement.to_bytes();
6540 let retirement_ref = StoreDeviceSelfRetirementRef::from_retirement(
6541 &retirement,
6542 exact(
6543 format!(
6544 "{}.json",
6545 device_self_retirement_semantic_prefix(
6546 candidate_family,
6547 &fixture.registration_ref.device_id,
6548 retirement.retirement_hash(),
6549 )
6550 ),
6551 &retirement_bytes,
6552 ),
6553 );
6554 assert_eq!(
6555 StoreDeviceSelfRetirement::parse_at(
6556 &retirement_bytes,
6557 &retirement_ref,
6558 &fixture.registration,
6559 )
6560 .unwrap(),
6561 retirement
6562 );
6563 let commit = StoreBatchCommit::signed_with_self_retirement(
6564 fixture.root_ref.store_root_hash,
6565 write_id,
6566 fixture.commit_ref.coord.clone(),
6567 fixture.registration_ref.clone(),
6568 &fixture.registration,
6569 order,
6570 fixture.commit.membership_state.clone(),
6571 fixture.commit.device_state.clone(),
6572 None,
6573 retirement_ref.clone(),
6574 &device_signer,
6575 )
6576 .unwrap();
6577 assert_eq!(
6578 commit.device_retirements(),
6579 std::slice::from_ref(&retirement_ref)
6580 );
6581
6582 let active = ResolvedStoreDeviceState::founder(
6583 &fixture.root_ref,
6584 fixture.registration_ref,
6585 &fixture.root.descriptor.founder_pubkey,
6586 fixture.root.descriptor.founder_grant.clone(),
6587 &fixture.root.descriptor.founder_recovery,
6588 )
6589 .unwrap();
6590 let retired = active.clone().self_retire(retirement_ref).unwrap();
6591 let merged = ResolvedStoreDeviceState::merge([active, retired]).unwrap();
6592 assert!(matches!(
6593 merged
6594 .devices
6595 .values()
6596 .next()
6597 .expect("founder device")
6598 .status,
6599 StoreDeviceStatus::Inactive { .. }
6600 ));
6601 }
6602
6603 #[test]
6604 fn serial_membership_and_rotation_are_authenticated_by_the_global_commit() {
6605 let fixture = fixture();
6606 let member = UserKeypair::generate();
6607 let state = crate::sync::membership::SerialMembershipState::from_founder(
6608 fixture.root_ref.store_root_hash,
6609 &founder_entry(
6610 &fixture.root_ref.store_root_id.to_string(),
6611 &fixture.signer,
6612 fixture.root.descriptor.founder_grant.clone(),
6613 "0000000001000-0000-device-a",
6614 GrantStreamAnchor::StoreMembership {
6615 first_slot: slot("store-v1/membership/founder/1.json".to_string()),
6616 },
6617 fixture.root.descriptor.founder_provider_admin.clone(),
6618 ),
6619 )
6620 .unwrap();
6621 let entry = state
6622 .signed_set_member(
6623 &fixture.signer,
6624 keys::public_key_hex(&member),
6625 None,
6626 crate::sync::membership::MemberRole::Member,
6627 "add".to_string(),
6628 )
6629 .unwrap();
6630 let device_signer = fixture.registration.device_signer(&fixture.signer).unwrap();
6631 let commit = StoreBatchCommit::signed_with_control(
6632 fixture.root_ref.store_root_hash,
6633 WriteId::from_generated("serial-control-write".to_string()),
6634 fixture.commit_ref.coord.clone(),
6635 fixture.registration_ref,
6636 &fixture.registration,
6637 fixture.commit.order,
6638 fixture.commit.membership_state,
6639 fixture.commit.device_state,
6640 None,
6641 Some(StoreControl::SerialMembershipAndKeyRotation {
6642 entry: entry.clone(),
6643 generation: 2,
6644 }),
6645 None,
6646 &device_signer,
6647 )
6648 .unwrap();
6649 let parsed = StoreBatchCommit::parse_at(
6650 &commit.to_bytes(),
6651 fixture.root_ref.store_root_hash,
6652 &fixture.commit_ref.coord,
6653 &fixture.registration,
6654 )
6655 .unwrap();
6656 assert_eq!(parsed, commit);
6657 assert!(state
6658 .apply(
6659 parsed
6660 .control()
6661 .unwrap()
6662 .serial_membership_entry()
6663 .expect("membership control")
6664 )
6665 .is_ok());
6666 }
6667}