Skip to main content

coven_core/sync/
store_commit.rs

1//! Signed, hash-addressed Store commit protocol objects.
2
3use 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/// Closed coordinate of one Store commit under the Store's signed policy.
166#[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/// Domain-separated family shared by replacements at one competition point.
203#[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/// Exact identity of one signed Store commit candidate.
279#[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/// Exact stored candidate commit retained as cleanup authority after abandonment.
288#[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/// Exact materialized cut, shaped by the Store's signed write policy.
464#[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/// Exact Store history cut, including the signed Serial genesis authority.
472#[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/// Predecessor and dependency order authenticated by one Store commit.
594#[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/// Exact stored head plus its reserved successor slot.
718#[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/// Exact recipient-visible access envelope paired with its sealed leaf.
726#[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/// Exact recipient-sealed access-leaf object named by a Store activation.
738#[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/// Exact Circle-metadata object and the epoch key that must open it.
757#[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/// Closed exact object graph needed to verify one Store-activated Circle control.
765#[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/// Exact Recovery activation carried by the first Serial commit authored by
894/// the replacement device.
895#[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/// Signed global activation point for a Serial Store.
2691#[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            &registration.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            &registration.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(&registration.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, &registration.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 &registration.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(&registration.device_id.to_string()),
4226            });
4227        }
4228        if registration.device_id
4229            != StoreDeviceId::derive(&registration.store_root, &registration.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            &registration.store_commits,
4237            &registration.acknowledgements,
4238            &registration.snapshots,
4239        )?;
4240        if !keys::verify_signature_hex(
4241            &registration.author_pubkey,
4242            &registration.identity_signature,
4243            &registration.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            &registration,
5544            exact(
5545                format!(
5546                    "{}.json",
5547                    registration_semantic_prefix(&registration.device_id.to_string())
5548                ),
5549                &registration_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            &registration_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            &registration,
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            &registration,
5811            ExactObjectRef::new(
5812                registration_slot.clone(),
5813                registration.to_bytes().len() as u64,
5814                ObjectHash::digest(&registration.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                    &registration_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            &registration,
5962            &device_signer,
5963        )
5964        .unwrap();
5965
5966        assert!(matches!(
5967            proof.verify(&attempt_ref, &attempt, &registration, &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                    &registration_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}