coven_database/store/store_session/
pending_publication.rs1use super::{publication_state::PreparedStoreWriteState, StoreDatabase, StoreSession};
2use crate::{
3 load_prepared_audience_objects_on, DbError, ExactProtocolObject, PreparedStoreWriteCommit,
4};
5use coven_protocol::membership::AuthorStreamId;
6use coven_protocol::store_commit::{
7 CommitFrontier, StoreBatchCommit, StoreBatchCommitRef, StoreCommitCoord,
8 StoreDeviceRegistrationRef, VerifiedStoreBatchCommit,
9};
10use coven_protocol::write::WriteId;
11use rusqlite::OptionalExtension;
12use std::collections::BTreeMap;
13
14pub struct LocalCommitBase {
24 authorship: super::OwnStreamAuthorship,
25 state: LocalCommitState,
26}
27
28pub struct LocalCommitState {
31 predecessor: Option<StoreBatchCommitRef>,
32 frontier: BTreeMap<String, StoreBatchCommitRef>,
33 membership: crate::InitialStoreMembershipAuthority,
34 publication: crate::StorePublicationBoundary,
35}
36
37impl LocalCommitBase {
38 pub fn into_parts(self) -> (super::OwnStreamAuthorship, LocalCommitState) {
39 (self.authorship, self.state)
40 }
41}
42
43impl LocalCommitState {
44 pub fn into_parts(
45 self,
46 ) -> (
47 Option<StoreBatchCommitRef>,
48 BTreeMap<String, StoreBatchCommitRef>,
49 crate::InitialStoreMembershipAuthority,
50 crate::StorePublicationBoundary,
51 ) {
52 (
53 self.predecessor,
54 self.frontier,
55 self.membership,
56 self.publication,
57 )
58 }
59}
60
61impl super::OwnStreamAuthorship {
62 pub async fn local_commit_base(
64 self,
65 stream_id: AuthorStreamId,
66 ) -> Result<LocalCommitBase, DbError> {
67 let state = self.read_local_commit_state(stream_id).await?;
68 Ok(LocalCommitBase {
69 authorship: self,
70 state,
71 })
72 }
73
74 pub async fn read_local_commit_state(
76 &self,
77 stream_id: AuthorStreamId,
78 ) -> Result<LocalCommitState, DbError> {
79 self.database
80 .call_store(move |session| session.local_commit_ledger_base(&stream_id))
81 .await
82 }
83}
84
85impl StoreSession<'_> {
86 fn local_commit_ledger_base(
87 &self,
88 stream_id: &AuthorStreamId,
89 ) -> Result<LocalCommitState, DbError> {
90 let stream_id = stream_id.to_string();
91 Ok(LocalCommitState {
92 predecessor: crate::store::materialized_commit_index::latest_position_for_device_on(
93 self.conn, &stream_id,
94 )?,
95 frontier: crate::store::materialized_commit_index::materialized_frontier_on(
96 self.conn, None,
97 )?,
98 membership: crate::InitialStoreMembershipAuthority::load_on(self.conn)?,
99 publication: super::observed_store_publication::load_store_current_publication_on(
100 self.conn,
101 )?,
102 })
103 }
104
105 fn latest_local_store_position(
106 &self,
107 stream_id: &str,
108 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
109 crate::store::materialized_commit_index::latest_position_for_device_on(self.conn, stream_id)
110 }
111
112 fn oldest_prepared_store_write(&mut self) -> Result<Option<PreparedStoreWriteCommit>, DbError> {
113 let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
114 let row = self
115 .conn
116 .query_row(
117 "SELECT write_id, base, prepared FROM store_writes
118 WHERE prepared IS NOT NULL
119 AND status = '\"publishing\"'
120 ORDER BY ordinal LIMIT 1",
121 [],
122 |row| {
123 Ok((
124 row.get::<_, String>(0)?,
125 row.get::<_, Option<String>>(1)?,
126 row.get::<_, String>(2)?,
127 ))
128 },
129 )
130 .optional()
131 .map_err(DbError::from)?;
132 row.map(|(write_id, base, prepared)| {
133 let base = base.ok_or_else(|| {
134 DbError::Message(format!(
135 "publishing write {write_id} carries no commit base"
136 ))
137 })?;
138 let prepared: PreparedStoreWriteState = serde_json::from_str(&prepared)
139 .map_err(|error| DbError::context("prepared Store write", error))?;
140 let PreparedStoreWriteState { commit, .. } = &prepared;
141 let write_id = WriteId::from_generated(write_id);
142 let unverified_commit: StoreBatchCommit =
143 serde_json::from_slice(commit.semantic_bytes())
144 .map_err(|error| DbError::context("prepared Store commit", error))?;
145 if unverified_commit.write_id != write_id {
146 return Err(DbError::Message(
147 "prepared write id differs from signed commit".to_string(),
148 ));
149 }
150 let registration_ref = &unverified_commit.author_registration;
151 let stored_registration_ref: String = self
152 .conn
153 .query_row(
154 "SELECT registration_object \
155 FROM store_device_registration_activations \
156 WHERE device_id = ?1 AND registration_hash = ?2",
157 (
158 registration_ref.device_id.to_string(),
159 registration_ref.registration_hash.to_string(),
160 ),
161 |row| row.get(0),
162 )
163 .map_err(DbError::from)?;
164 let stored_registration_ref: StoreDeviceRegistrationRef =
165 serde_json::from_str(&stored_registration_ref)
166 .map_err(|error| DbError::context("prepared write registration ref", error))?;
167 if stored_registration_ref != *registration_ref {
168 return Err(DbError::Message(
169 "prepared commit registration differs from its activation".to_string(),
170 ));
171 }
172 let authority = self.activated_registration(registration_ref)?;
173 let registration = authority.value();
174 let root = ®istration.store_root;
175 let stream_id =
176 coven_protocol::store_commit::StreamActivation::device_authorized_stream_id(
177 root.store_root_hash,
178 registration_ref,
179 coven_protocol::store_commit::StreamAnchorDomain::StoreAnnouncements,
180 );
181 let coord = StoreCommitCoord {
182 stream_id,
183 sequence: unverified_commit.seq(),
184 };
185 let commit_value = VerifiedStoreBatchCommit::parse_prepared(
186 commit.semantic_bytes(),
187 root.store_root_hash,
188 coord,
189 commit.prepared().reference().clone(),
190 registration,
191 )
192 .map_err(|error| DbError::context("verify prepared Store commit", error))?;
193 let commit_ref = commit_value.reference().clone();
194 let active =
195 super::active_store_publication::load_active_store_publication_on(self.conn)?
196 .ok_or_else(|| {
197 DbError::Message(format!(
198 "publishing write {write_id} has no active Store publication"
199 ))
200 })?;
201 if active.owner() != &crate::ActiveStorePublicationOwner::StoreWrite(write_id.clone())
202 || active.commit_reservation()
203 != Some((&write_id, registration_ref, &commit_ref.coord))
204 {
205 return Err(DbError::Message(format!(
206 "publishing write {write_id} differs from active Store publication {:?}",
207 active.owner()
208 )));
209 }
210 let publication = active.attempt()?;
211 publication
212 .verify_commit(&commit_value)
213 .map_err(|error| DbError::context("verify prepared Store publication", error))?;
214 let base = records.effective_store_write_base(&write_id, &base)?;
215 let mut dependencies = CommitFrontier::from_refs(base.dependencies)
216 .map_err(|error| DbError::context("prepared dependency frontier", error))?;
217 let observed_predecessor = dependencies.0.remove(&stream_id);
218 if dependencies.commits() != commit_value.merge_dependencies() {
219 return Err(DbError::Message(
220 "prepared commit differs from its write dependency frontier".to_string(),
221 ));
222 }
223 if observed_predecessor.as_ref().is_some_and(|captured| {
224 commit_value.order.predecessor().is_none_or(|current| {
225 current.coord.sequence() < captured.coord.sequence()
226 || current.coord.sequence() == captured.coord.sequence()
227 && current != captured
228 })
229 }) {
230 return Err(DbError::Message(
231 "prepared commit predecessor does not cover its write capture frontier"
232 .to_string(),
233 ));
234 }
235 let partitions = records.store_write_partitions(write_id.as_str())?;
236 let audiences =
237 load_prepared_audience_objects_on(self.conn, self.store_dir, &write_id)?;
238 let graph_commit = &commit_value;
239 let expected_package_count = usize::from(graph_commit.store_package().is_some())
240 .checked_add(graph_commit.circle_packages().len())
241 .ok_or_else(|| DbError::Message("package count overflow".to_string()))?;
242 if audiences.packages.len() != expected_package_count
243 || audiences.packages.len()
244 != usize::from(partitions.store.is_some()) + partitions.circles.len()
245 {
246 return Err(DbError::Message(
247 "prepared package indexes do not exactly cover commit audiences".to_string(),
248 ));
249 }
250 for package in &audiences.packages {
251 let value = package.package();
252 if value.write_id() != &write_id
253 || value.commit_coord() != &commit_ref.coord
254 || value.candidate_family() != commit_value.candidate_family()
255 {
256 return Err(DbError::Message(
257 "indexed audience package differs from its exact commit".to_string(),
258 ));
259 }
260 let expected_object = match value.audience() {
261 coven_protocol::audience_package::PackageAudience::Store => {
262 graph_commit
263 .verify_store_package(package.semantic_bytes())
264 .map_err(DbError::from)?;
265 &graph_commit
266 .store_package()
267 .as_ref()
268 .expect("verified present")
269 .object
270 }
271 coven_protocol::audience_package::PackageAudience::Circle {
272 circle_id, ..
273 } => {
274 graph_commit
275 .verify_circle_package(*circle_id, package.semantic_bytes())
276 .map_err(DbError::from)?;
277 &graph_commit
278 .circle_packages()
279 .iter()
280 .find(|entry| entry.circle_id == *circle_id)
281 .expect("verified present")
282 .package
283 .object
284 }
285 };
286 if package.object() != expected_object {
287 return Err(DbError::Message(
288 "indexed audience package exact object differs from its commit".to_string(),
289 ));
290 }
291 }
292 for package in &audiences.packages {
293 let audience = package.package().audience().remote_audience();
294 for binding in package.package().blob_bindings() {
295 if !audiences
296 .blobs
297 .iter()
298 .any(|blob| blob.audience() == &audience && blob.blob() == binding.blob())
299 {
300 return Err(DbError::Message(
301 "prepared package blob binding has no exact blob index".to_string(),
302 ));
303 }
304 }
305 }
306 for blob in &audiences.blobs {
307 if !audiences.packages.iter().any(|package| {
308 package.package().audience().remote_audience() == *blob.audience()
309 && package
310 .package()
311 .blob_bindings()
312 .iter()
313 .any(|binding| binding.blob() == blob.blob())
314 }) {
315 return Err(DbError::Message(
316 "prepared blob index has no exact package binding".to_string(),
317 ));
318 }
319 }
320 Ok(PreparedStoreWriteCommit {
321 audiences,
322 commit: ExactProtocolObject {
323 value: commit_value,
324 bytes: commit.semantic_bytes().to_vec(),
325 prepared: commit.prepared().clone(),
326 },
327 publication: publication.clone(),
328 })
329 })
330 .transpose()
331 }
332}
333
334impl StoreDatabase {
335 pub async fn oldest_prepared_store_write(
336 &self,
337 ) -> Result<Option<PreparedStoreWriteCommit>, DbError> {
338 let (loaded, covered) = self
339 .call_store(move |session| {
340 let loaded = session.oldest_prepared_store_write()?;
341 let covered = match &loaded {
342 Some(batch) => super::StoreRecords::new(session.conn, session.store_dir)
343 .covered_store_write(&batch.commit.value)?
344 .is_some(),
345 None => false,
346 };
347 Ok((loaded, covered))
348 })
349 .await?;
350 if !covered {
354 if let Some(batch) = &loaded {
355 for blob in &batch.audiences.blobs {
356 if let Some(spool_path) = blob.spool_path() {
357 {
358 let (size, digest) =
359 coven_foundation::local_file::file_facts(spool_path)
360 .await
361 .map_err(|error| {
362 DbError::context("prepared blob spool", error)
363 })?;
364 blob.blob()
365 .object()
366 .verify_stored_facts(
367 spool_path,
368 size,
369 coven_protocol::store_commit::ObjectHash::from_digest(digest),
370 )
371 .map_err(|error| DbError::context("prepared blob spool", error))?;
372 }
373 }
374 }
375 }
376 }
377 Ok(loaded)
378 }
379
380 pub async fn local_commit_base(
397 &self,
398 stream_id: AuthorStreamId,
399 ) -> Result<LocalCommitBase, DbError> {
400 self.author_own_stream()
401 .await
402 .local_commit_base(stream_id)
403 .await
404 }
405
406 pub async fn latest_local_store_position(
407 &self,
408 stream_id: AuthorStreamId,
409 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
410 self.call_store(move |session| session.latest_local_store_position(&stream_id.to_string()))
411 .await
412 }
413}