Skip to main content

coven_database/store/store_session/
circle_snapshot_publication.rs

1use std::collections::BTreeSet;
2
3use crate::*;
4use coven_protocol::circle::CircleId;
5use coven_protocol::store_commit::{
6    circle_snapshot_image_semantic_prefix, circle_snapshot_slot_prefix, CircleSnapshotMeta,
7    CircleSnapshotRef,
8};
9use rusqlite::OptionalExtension;
10
11use super::*;
12
13impl StoreSession<'_> {
14    fn pending_circle_snapshot_ids(&self) -> Result<Vec<CircleId>, DbError> {
15        let mut statement = self
16            .conn
17            .prepare("SELECT circle_id FROM outbound_circle_snapshot ORDER BY circle_id")
18            .map_err(DbError::from)?;
19        let rows = statement
20            .query_map([], |row| row.get::<_, String>(0))
21            .map_err(DbError::from)?;
22        rows.map(|row| {
23            row.map_err(DbError::from)?
24                .parse()
25                .map_err(|error| DbError::context("pending Circle snapshot identity", error))
26        })
27        .collect()
28    }
29
30    fn outbound_circle_snapshot_publication(
31        &mut self,
32        circle_id: CircleId,
33    ) -> Result<Option<DurableCircleSnapshotPublication>, DbError> {
34        let authority = self.local_store_authority()?;
35        load_outbound_circle_snapshot_on(self.conn, self.store_dir, &authority, circle_id)
36    }
37
38    fn latest_local_circle_snapshot(
39        &mut self,
40        circle_id: CircleId,
41    ) -> Result<Option<PublishedCircleSnapshot>, DbError> {
42        let authority = self.local_store_authority()?;
43        load_published_circle_snapshot_on(self.conn, &authority, circle_id)
44    }
45
46    fn stage_circle_snapshot_publication(
47        &mut self,
48        meta: CircleSnapshotMeta,
49        meta_prepared: PreparedExactObject,
50        image: SnapshotDatabaseImage,
51        image_prepared: PreparedExactObject,
52    ) -> Result<CircleSnapshotRef, DbError> {
53        let authority = self.local_store_authority()?;
54        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
55        let image_facts =
56            crate::payload_store::write_payload_file_blocking(&tx, self.store_dir, image.path())
57                .map_err(|source| SnapshotImageError::ProjectionPayloadStore {
58                    operation: "spool Circle snapshot image".to_string(),
59                    source,
60                });
61        let (image_hash, _) = image.finish(image_facts).map_err(snapshot_image_db_error)?;
62        let image_prepared_hash = crate::payload_store::write_payload_blocking(
63            &tx,
64            self.store_dir,
65            image_prepared.stored_bytes(),
66        )
67        .map_err(|error| DbError::context("spool prepared Circle snapshot image", error))?;
68        let image_prepared_size = image_prepared.stored_bytes().len() as u64;
69        let registration_ref = authority.reference();
70        let registration = authority.value();
71        validate_snapshot_author(&meta.author_registration, registration_ref, "Circle")?;
72        let device_id = registration.device_id.to_string();
73        validate_snapshot_image(
74            &meta.bootstrap.image,
75            &image_prepared,
76            image_hash,
77            image_prepared_hash,
78            image_prepared_size,
79            format!(
80                "{}.db",
81                circle_snapshot_image_semantic_prefix(
82                    meta.circle_id,
83                    &device_id,
84                    meta.bootstrap.image.image_hash,
85                )
86            ),
87            "Circle",
88        )?;
89        let reference = CircleSnapshotRef {
90            generation: meta.generation,
91            snapshot_hash: meta.snapshot_hash(),
92            object: meta_prepared.reference().clone(),
93        };
94        CircleSnapshotMeta::parse_at(
95            &meta.to_bytes(),
96            registration.store_root.store_root_hash,
97            &reference,
98            registration,
99        )
100        .map_err(|error| DbError::context("verify staged Circle snapshot metadata", error))?;
101        let previous = load_published_circle_snapshot_on(&tx, &authority, meta.circle_id)?;
102        let (expected_generation, expected_slot) = match &previous {
103            Some(previous) => (
104                previous
105                    .reference
106                    .generation
107                    .checked_add(1)
108                    .ok_or_else(|| {
109                        DbError::Message("Circle snapshot generation overflow".to_string())
110                    })?,
111                previous.successor_slot.clone(),
112            ),
113            None => (
114                0,
115                coven_protocol::objects::ObjectSlot::logical(format!(
116                    "{}.json",
117                    circle_snapshot_slot_prefix(meta.circle_id, &device_id, 0)
118                ))
119                .map_err(DbError::from)?,
120            ),
121        };
122        if meta.generation != expected_generation
123            || meta_prepared.reference().slot() != &expected_slot
124            || meta.successor.predecessor != previous.as_ref().map(|value| value.reference.clone())
125        {
126            return Err(DbError::Message(
127                "Circle snapshot does not extend the exact local stream".to_string(),
128            ));
129        }
130        let next_generation = meta
131            .generation
132            .checked_add(1)
133            .ok_or_else(|| DbError::Message("Circle snapshot generation overflow".to_string()))?;
134        if meta.successor.next_slot.logical_key()
135            != format!(
136                "{}.json",
137                circle_snapshot_slot_prefix(meta.circle_id, &device_id, next_generation)
138            )
139        {
140            return Err(DbError::Message(
141                "Circle snapshot successor is outside its activated exact stream".to_string(),
142            ));
143        }
144        tx.execute(
145            "INSERT INTO outbound_circle_snapshot \
146             (circle_id, snapshot_ref, meta_prepared, meta_bytes) \
147             VALUES (?1, ?2, ?3, ?4)",
148            rusqlite::params![
149                meta.circle_id.to_string(),
150                serde_json::to_string(&reference).map_err(|error| {
151                    DbError::context("serialize exact Circle snapshot ref", error)
152                })?,
153                serde_json::to_string(&meta_prepared).map_err(|error| {
154                    DbError::context("serialize prepared Circle snapshot metadata", error)
155                })?,
156                meta.to_bytes(),
157            ],
158        )
159        .map_err(DbError::from)?;
160        crate::payload_store::set_payload_owner_claims_on(
161            &tx,
162            &crate::payload_store::outbound_circle_snapshot_owner_key(meta.circle_id),
163            &BTreeSet::from([image_hash, image_prepared_hash]),
164        )?;
165        tx.commit().map_err(DbError::from)?;
166        Ok(reference)
167    }
168
169    fn complete_circle_snapshot_publication(
170        &mut self,
171        accepted: CircleSnapshotRef,
172    ) -> Result<(), DbError> {
173        let authority = self.local_store_authority()?;
174        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
175        let circle_id = {
176            let bytes: Vec<u8> = tx
177                .query_row(
178                    "SELECT meta_bytes FROM outbound_circle_snapshot \
179                     WHERE snapshot_ref = ?1",
180                    [serde_json::to_string(&accepted).map_err(|error| {
181                        DbError::context("serialize accepted Circle snapshot ref", error)
182                    })?],
183                    |row| row.get::<_, Vec<u8>>(0),
184                )
185                .optional()
186                .map_err(DbError::from)?
187                .ok_or_else(|| {
188                    DbError::Message("outbound Circle snapshot is absent".to_string())
189                })?;
190            let meta: CircleSnapshotMeta = serde_json::from_slice(&bytes)
191                .map_err(|error| DbError::context("accepted Circle snapshot metadata", error))?;
192            meta.circle_id
193        };
194        let outbound =
195            load_outbound_circle_snapshot_on(&tx, self.store_dir, &authority, circle_id)?
196                .ok_or_else(|| {
197                    DbError::Message("outbound Circle snapshot is absent".to_string())
198                })?;
199        if outbound.reference != accepted {
200            return Err(DbError::Message(
201                "accepted Circle snapshot differs from the prepared exact object".to_string(),
202            ));
203        }
204        let snapshot_owner = coven_protocol::remote_object::SnapshotObjectOwner::Circle {
205            activation: coven_protocol::store_commit::circle_snapshot_stream_activation(
206                outbound.meta.value.store_root_hash,
207                &outbound.meta.value.author_registration,
208                outbound.meta.value.circle_id,
209            )
210            .map_err(DbError::from)?,
211            generation: outbound.meta.value.generation,
212        };
213        persist_snapshot_image_on(
214            &tx,
215            self.store_dir,
216            &outbound.meta.value.bootstrap.image,
217            snapshot_owner,
218            "Circle snapshot image",
219        )?;
220        let deleted = tx
221            .execute(
222                "DELETE FROM outbound_circle_snapshot WHERE circle_id = ?1",
223                [circle_id.to_string()],
224            )
225            .map_err(DbError::from)?;
226        if deleted != 1 {
227            return Err(DbError::Message(
228                "outbound Circle snapshot ownership row is absent or changed".to_string(),
229            ));
230        }
231        crate::payload_store::release_payload_owner_on(
232            &tx,
233            &crate::payload_store::outbound_circle_snapshot_owner_key(circle_id),
234        )?;
235        let accepted_generation =
236            snapshot_generation_as_i64(accepted.generation, "Circle snapshot")?;
237        tx.execute(
238            "INSERT INTO published_circle_snapshot \
239             (circle_id, generation, snapshot_ref, successor_slot, cut, meta_bytes) \
240             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
241            rusqlite::params![
242                circle_id.to_string(),
243                accepted_generation,
244                serde_json::to_string(&accepted).map_err(|error| {
245                    DbError::context("serialize published Circle snapshot ref", error)
246                })?,
247                serde_json::to_string(&outbound.meta.value.successor.next_slot).map_err(
248                    |error| DbError::context("serialize Circle snapshot successor slot", error)
249                )?,
250                serde_json::to_string(&outbound.meta.value.bootstrap.coverage)
251                    .map_err(|error| DbError::context("serialize Circle snapshot cut", error))?,
252                outbound.meta.bytes,
253            ],
254        )
255        .map_err(DbError::from)?;
256        tx.commit().map_err(DbError::from)
257    }
258}
259
260impl StoreDatabase {
261    pub async fn pending_circle_snapshot_ids(&self) -> Result<Vec<CircleId>, DbError> {
262        self.call_store(move |session| session.pending_circle_snapshot_ids())
263            .await
264    }
265
266    pub async fn outbound_circle_snapshot_publication(
267        &self,
268        circle_id: CircleId,
269    ) -> Result<Option<DurableCircleSnapshotPublication>, DbError> {
270        self.call_store(move |session| session.outbound_circle_snapshot_publication(circle_id))
271            .await
272    }
273
274    pub async fn latest_local_circle_snapshot(
275        &self,
276        circle_id: CircleId,
277    ) -> Result<Option<PublishedCircleSnapshot>, DbError> {
278        self.call_store(move |session| session.latest_local_circle_snapshot(circle_id))
279            .await
280    }
281
282    pub async fn stage_circle_snapshot_publication(
283        &self,
284        meta: CircleSnapshotMeta,
285        meta_prepared: PreparedExactObject,
286        image: SnapshotDatabaseImage,
287        image_prepared: PreparedExactObject,
288    ) -> Result<CircleSnapshotRef, DbError> {
289        self.call_store(move |session| {
290            session.stage_circle_snapshot_publication(meta, meta_prepared, image, image_prepared)
291        })
292        .await
293    }
294
295    pub async fn complete_circle_snapshot_publication(
296        &self,
297        accepted: CircleSnapshotRef,
298    ) -> Result<(), DbError> {
299        self.call_store(move |session| session.complete_circle_snapshot_publication(accepted))
300            .await
301    }
302}