Skip to main content

coven_protocol/store_commit/
circle_snapshot.rs

1use super::*;
2
3/// Exact coordinate of one signed Circle snapshot on its author's per-Circle
4/// snapshot stream.
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
6#[serde(deny_unknown_fields)]
7pub struct CircleSnapshotRef {
8    pub generation: u64,
9    pub snapshot_hash: ObjectHash,
10    pub object: ExactObjectRef,
11}
12
13/// The device-authorized activation binding one device's per-Circle snapshot
14/// stream to its Circle. Such a stream has no first slot in the registration —
15/// like the per-Circle acknowledgement stream, it is anchored on the deterministic
16/// generation-zero slot both the author and every reader compute.
17pub fn circle_snapshot_stream_activation(
18    store_root_hash: ObjectHash,
19    author_registration: &StoreDeviceRegistrationRef,
20    circle_id: CircleId,
21) -> Result<StreamActivationId, StoreProtocolError> {
22    let first_slot = ObjectSlot::logical(format!(
23        "{}.json",
24        circle_snapshot_slot_prefix(circle_id, &author_registration.device_id.to_string(), 0)
25    ))?;
26    Ok(StreamActivation::device_authorized(
27        store_root_hash,
28        author_registration.clone(),
29        DeviceStreamAnchor::CircleSnapshots {
30            circle_id,
31            first_slot,
32        },
33    )
34    .activation_id())
35}
36
37/// The exact predecessor and create-once successor slot binding one Circle
38/// snapshot into its per-(device, Circle) stream.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct CircleSnapshotSuccessorLink {
42    pub predecessor: Option<CircleSnapshotRef>,
43    pub next_slot: ObjectSlot,
44}
45
46/// One device's signed, Circle-sealed snapshot of the private Circle history it
47/// holds at an exact Store frontier. The installable payload is a
48/// `CircleBootstrapRef` — the same image format a member-addition bootstrap
49/// carries — so a verifier installs a snapshot with the bootstrap machinery. The
50/// metadata additionally binds the exact control, epoch, and key fingerprint the
51/// image derives from and the per-(device, Circle) snapshot stream position.
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct CircleSnapshotMetaBody {
55    pub store_root_hash: ObjectHash,
56    pub circle_id: CircleId,
57    pub author_registration: StoreDeviceRegistrationRef,
58    pub control: CircleControlCoord,
59    pub epoch_id: CircleEpochId,
60    pub key_fingerprint: KeyFingerprint,
61    pub generation: u64,
62    /// The exact cut, schema, routing hash, image, and pinned blob refs the
63    /// image contains — the same shape a member-addition bootstrap carries. The
64    /// cut is `bootstrap.coverage`.
65    pub bootstrap: CircleBootstrapRef,
66    pub created_at: String,
67    pub successor: CircleSnapshotSuccessorLink,
68}
69
70impl SignedBody for CircleSnapshotMetaBody {
71    const DOMAIN: &'static [u8] = CIRCLE_SNAPSHOT_DOMAIN;
72}
73
74pub type CircleSnapshotMeta = Signed<CircleSnapshotMetaBody>;
75
76impl CircleSnapshotMeta {
77    #[allow(clippy::too_many_arguments)]
78    pub fn signed(
79        store_root_hash: ObjectHash,
80        circle_id: CircleId,
81        author_registration: StoreDeviceRegistrationRef,
82        control: CircleControlCoord,
83        epoch_id: CircleEpochId,
84        key_fingerprint: KeyFingerprint,
85        generation: u64,
86        bootstrap: CircleBootstrapRef,
87        created_at: String,
88        successor: CircleSnapshotSuccessorLink,
89        device_signer: &UserKeypair,
90    ) -> Result<Self, StoreProtocolError> {
91        validate_circle_snapshot_generation(generation, successor.predecessor.as_ref())?;
92        validate_circle_snapshot_state(&control, &bootstrap.coverage)?;
93        Ok(Signed::sign(
94            CircleSnapshotMetaBody {
95                store_root_hash,
96                circle_id,
97                author_registration,
98                control,
99                epoch_id,
100                key_fingerprint,
101                generation,
102                bootstrap,
103                created_at,
104                successor,
105            },
106            device_signer,
107        ))
108    }
109
110    pub fn snapshot_hash(&self) -> ObjectHash {
111        self.hash()
112    }
113
114    pub fn semantic_hash_from_bytes(bytes: &[u8]) -> Result<ObjectHash, StoreProtocolError> {
115        let meta: Self = crate::objects::decode_protocol_object(bytes)?;
116        Ok(meta.snapshot_hash())
117    }
118
119    /// Verify one exact Circle snapshot against its expected reference and author
120    /// registration. The author and Circle determine the stream identity; the
121    /// reader follows its deterministic generation-zero slot and verifies the
122    /// exact predecessor chain to establish each snapshot's stream position.
123    pub fn parse_at(
124        bytes: &[u8],
125        expected_store_root_hash: ObjectHash,
126        expected: &CircleSnapshotRef,
127        author: &StoreDeviceRegistration,
128    ) -> Result<Self, StoreProtocolError> {
129        let meta: Self = crate::objects::decode_protocol_object(bytes)?;
130        meta.require_version()?;
131        crate::objects::verify_store_root(expected_store_root_hash, meta.store_root_hash)?;
132        crate::objects::verify_store_root(
133            expected_store_root_hash,
134            author.store_root.store_root_hash,
135        )?;
136        meta.author_registration.verify_registration(author)?;
137        if meta.generation != expected.generation {
138            return Err(StoreProtocolError::RelocatedSlot {
139                expected: circle_snapshot_slot_prefix(
140                    meta.circle_id,
141                    &author.device_id.to_string(),
142                    expected.generation,
143                ),
144                actual: circle_snapshot_slot_prefix(
145                    meta.circle_id,
146                    &author.device_id.to_string(),
147                    meta.generation,
148                ),
149            });
150        }
151        validate_circle_snapshot_generation(meta.generation, meta.successor.predecessor.as_ref())?;
152        validate_circle_snapshot_state(&meta.control, &meta.bootstrap.coverage)?;
153        meta.verify_by(&author.device_signing_pubkey)?;
154        let actual = meta.snapshot_hash();
155        if actual != expected.snapshot_hash {
156            return Err(StoreProtocolError::ObjectHashMismatch {
157                expected: expected.snapshot_hash,
158                actual,
159            });
160        }
161        Ok(meta)
162    }
163}
164
165fn validate_circle_snapshot_state(
166    control: &CircleControlCoord,
167    coverage: &CommitFrontier,
168) -> Result<(), StoreProtocolError> {
169    control.validate()?;
170    super::validation::validate_commit_frontier(coverage)
171}
172
173fn validate_circle_snapshot_generation(
174    generation: u64,
175    predecessor: Option<&CircleSnapshotRef>,
176) -> Result<(), StoreProtocolError> {
177    match (generation, predecessor) {
178        (0, None) => Ok(()),
179        (0, Some(_)) | (_, None) => Err(StoreProtocolError::Malformed(
180            "Circle snapshot generation and predecessor disagree".to_string(),
181        )),
182        (generation, Some(predecessor)) => {
183            let expected = predecessor.generation.checked_add(1).ok_or_else(|| {
184                StoreProtocolError::Malformed("Circle snapshot generation overflow".to_string())
185            })?;
186            if generation != expected {
187                return Err(StoreProtocolError::Malformed(
188                    "Circle snapshot generation does not follow its predecessor".to_string(),
189                ));
190            }
191            Ok(())
192        }
193    }
194}