1use super::{
2 publication_state::{
3 MergeAbandonmentOutcome, MergeCandidateAbandonmentPreparation, PreparedStoreWriteState,
4 StoreWritePreparation,
5 },
6 StoreDatabase, StoreSession,
7};
8use crate::{
9 persist_exact_remote_object_on, DbError, DurablePreparedProtocolObject, StoreWriteBase,
10 LOCAL_DEVICE_ID_STATE_KEY,
11};
12use coven_protocol::remote_object::RemoteObjectRecord;
13use coven_protocol::store_commit::{
14 CommitFrontier, StoreCommitCoord, StoreDeviceHead, StoreDeviceRegistrationRef,
15};
16use coven_protocol::write::WriteStatus;
17use rusqlite::OptionalExtension;
18
19impl StoreSession<'_> {
20 fn table_schema_for_apply(&mut self) -> Result<crate::TableSchema, DbError> {
21 crate::TableSchema::for_apply(self.conn, self.synced_tables, self.gates)
22 }
23
24 fn prepare_store_write_commit(&mut self, stage: StoreWritePreparation) -> Result<(), DbError> {
25 let author = self.activated_registration(&stage.commit.value.author_registration)?;
26 if author.value().store_root != stage.root {
27 return Err(DbError::Message(
28 "prepared Store write belongs to another verified Store root".to_string(),
29 ));
30 }
31 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
32 let local_device_id = crate::required_protocol_state_on(&tx, LOCAL_DEVICE_ID_STATE_KEY)?;
33 let registration_object: String = tx
34 .query_row(
35 "SELECT registration_object \
36 FROM store_device_registration_activations WHERE device_id = ?1",
37 [&local_device_id],
38 |row| row.get(0),
39 )
40 .map_err(DbError::from)?;
41 let registration_ref: StoreDeviceRegistrationRef =
42 serde_json::from_str(®istration_object).map_err(|error| {
43 DbError::context("prepared write exact registration ref", error)
44 })?;
45 if registration_ref != stage.commit.value.author_registration
46 || registration_ref != stage.head.value.author_registration
47 {
48 return Err(DbError::Message(
49 "prepared Store commit/head author registration differs from local activation"
50 .to_string(),
51 ));
52 }
53 let registration = stage.commit.value.author();
54 if author.value() != registration {
55 return Err(DbError::Message(
56 "prepared write author registration differs from its activated bytes".to_string(),
57 ));
58 }
59 let stream_id = coven_protocol::store_commit::StreamActivation::device_authorized_stream_id(
60 stage.root.store_root_hash,
61 ®istration_ref,
62 coven_protocol::store_commit::StreamAnchorDomain::StoreAnnouncements,
63 );
64 let expected_coord = StoreCommitCoord {
65 stream_id,
66 sequence: stage.commit.value.seq(),
67 };
68 if stage.commit.value.store_root_hash() != stage.root.store_root_hash
69 || stage.commit.value.reference().coord != expected_coord
70 || stage.commit.value.reference().object != *stage.commit.prepared.reference()
71 {
72 return Err(DbError::Message(
73 "authenticated prepared Store commit differs from its current local authority"
74 .to_string(),
75 ));
76 }
77 if stage.commit.value.write_id != stage.write_id {
78 return Err(DbError::Message(
79 "prepared write id differs from signed commit".to_string(),
80 ));
81 }
82 let commit_ref = stage.commit.value.reference().clone();
83 if stage.head.value.commit != commit_ref {
84 return Err(DbError::Message(
85 "prepared Store head does not activate the exact prepared commit".to_string(),
86 ));
87 }
88 stage
89 .history_evidence
90 .validate_for(&commit_ref, stage.commit.value.value())
91 .map_err(|error| DbError::context("prepared Store history evidence", error))?;
92 StoreDeviceHead::parse_at(
93 &stage.head.value.to_bytes(),
94 stage.root.store_root_hash,
95 registration,
96 &commit_ref,
97 )
98 .map_err(|error| DbError::context("verify prepared Store head", error))?;
99 let (stored_base, stored_status, stored_preparation): (
100 Option<String>,
101 String,
102 Option<String>,
103 ) = tx
104 .query_row(
105 "SELECT base, status, prepared
106 FROM store_writes WHERE write_id = ?1",
107 [stage.write_id.as_str()],
108 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
109 )
110 .map_err(DbError::from)?;
111 if stored_status != "\"pending\"" || stored_preparation.is_some() {
112 return Err(DbError::Message(format!(
113 "write {} is not an unprepared pending write",
114 stage.write_id
115 )));
116 }
117 let stored_base = stored_base.ok_or_else(|| {
118 DbError::Message(format!(
119 "pending write {} carries no commit base",
120 stage.write_id
121 ))
122 })?;
123 let partitions = crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
124 .store_write_partitions(stage.write_id.as_str())?;
125 let stored_base: StoreWriteBase = serde_json::from_str(&stored_base)
126 .map_err(|error| DbError::context(format!("write {} base", stage.write_id), error))?;
127 let mut stored_dependencies = CommitFrontier::from_refs(stored_base.dependencies)
128 .map_err(|error| DbError::context("stored write dependencies", error))?;
129 let observed_predecessor = stored_dependencies.0.remove(&stream_id);
130 if stored_dependencies.commits() != stage.commit.value.merge_dependencies() {
131 return Err(DbError::Message(format!(
132 "prepared commit dependencies differ from write {}",
133 stage.write_id
134 )));
135 }
136 let another_prepared: Option<String> = tx
137 .query_row(
138 "SELECT write_id FROM store_writes
139 WHERE prepared IS NOT NULL AND write_id != ?1
140 ORDER BY ordinal LIMIT 1",
141 [stage.write_id.as_str()],
142 |row| row.get(0),
143 )
144 .optional()
145 .map_err(DbError::from)?;
146 if let Some(other_write_id) = another_prepared {
147 return Err(DbError::Message(format!(
148 "write {other_write_id} already owns Store publication"
149 )));
150 }
151 let durable_predecessor =
152 crate::store::materialized_commit_index::latest_position_for_device_on(
153 &tx,
154 &stream_id.to_string(),
155 )?;
156 if observed_predecessor.as_ref().is_some_and(|observed| {
157 durable_predecessor.as_ref().is_none_or(|current| {
158 current.coord.sequence() < observed.coord.sequence()
159 || current.coord.sequence() == observed.coord.sequence() && current != observed
160 })
161 }) {
162 return Err(DbError::Message(format!(
163 "outbound Store commit predecessor does not cover write {} capture frontier",
164 stage.write_id
165 )));
166 }
167 let expected_seq = durable_predecessor
168 .as_ref()
169 .map_or(1, |reference| reference.coord.sequence().saturating_add(1));
170 if stage.commit.value.seq() != expected_seq
171 || stage.commit.value.order.predecessor() != durable_predecessor.as_ref()
172 {
173 return Err(DbError::Message(format!(
174 "outbound Store commit exact predecessor differs from durable {durable_predecessor:?}"
175 )));
176 }
177
178 let mut object_ids = std::collections::BTreeSet::new();
179 for remote in &stage.remote_objects {
180 remote
181 .validate()
182 .map_err(|error| DbError::context("prepared remote object", error))?;
183 if !object_ids.insert(remote.object_id()) {
184 return Err(DbError::Message(
185 "prepared write contains a duplicate remote object".to_string(),
186 ));
187 }
188 }
189 crate::validate_prepared_audience_blob_graph(&object_ids, &stage.audiences)?;
190 for remote in &stage.remote_objects {
191 crate::persist_prepared_remote_object_on(
192 &tx,
193 self.store_dir,
194 remote,
195 &commit_ref,
196 "candidate audience object",
197 )?;
198 }
199 let commit_remote = RemoteObjectRecord::candidate_commit(
200 commit_ref.clone(),
201 &stage.commit.value.to_bytes(),
202 stage.commit.prepared.stored_bytes(),
203 )
204 .map_err(|error| DbError::context("prepared candidate commit", error))?;
205 persist_exact_remote_object_on(&tx, self.store_dir, &commit_remote, "candidate commit")?;
206 let expected_partition_count = usize::from(partitions.store.is_some())
207 .checked_add(partitions.circles.len())
208 .ok_or_else(|| DbError::Message("audience partition count overflow".to_string()))?;
209 if stage.audiences.packages.len() != expected_partition_count {
210 return Err(DbError::Message(
211 "prepared audience packages do not cover every write partition".to_string(),
212 ));
213 }
214 let mut indexed = std::collections::BTreeSet::new();
215 for package in &stage.audiences.packages {
216 let value = package.package();
217 if value.store_root_hash() != stage.root.store_root_hash
218 || value.write_id() != &stage.write_id
219 || value.commit_coord() != &commit_ref.coord
220 || value.candidate_family() != stage.commit.value.candidate_family()
221 {
222 return Err(DbError::Message(
223 "prepared audience package differs from its exact Store commit".to_string(),
224 ));
225 }
226 match value.audience() {
227 coven_protocol::audience_package::PackageAudience::Store => {
228 let partition = partitions.store.as_ref().ok_or_else(|| {
229 DbError::Message(
230 "prepared Store package has no Store partition".to_string(),
231 )
232 })?;
233 if value.changeset() != partition.changeset {
234 return Err(DbError::Message(
235 "prepared Store package changeset differs from its partition"
236 .to_string(),
237 ));
238 }
239 stage
240 .commit
241 .value
242 .verify_store_package(package.semantic_bytes())
243 .map_err(DbError::from)?;
244 }
245 coven_protocol::audience_package::PackageAudience::Circle { circle_id, .. } => {
246 let partition = partitions
247 .circles
248 .iter()
249 .find(|partition| {
250 partition.audience
251 == coven_protocol::circle::Audience::Circle(*circle_id)
252 })
253 .ok_or_else(|| {
254 DbError::Message(format!(
255 "prepared Circle package {circle_id} has no partition"
256 ))
257 })?;
258 if value.changeset() != partition.changeset {
259 return Err(DbError::Message(format!(
260 "prepared Circle package {circle_id} changeset differs from its partition"
261 )));
262 }
263 stage
264 .commit
265 .value
266 .verify_circle_package(*circle_id, package.semantic_bytes())
267 .map_err(DbError::from)?;
268 }
269 }
270 indexed.insert(package.remote_object_id());
271 }
272 indexed.extend(
273 stage
274 .audiences
275 .blobs
276 .iter()
277 .map(crate::PreparedAudienceBlob::remote_object_id),
278 );
279 debug_assert_eq!(indexed, object_ids);
280 super::prepared_remote_objects::persist_prepared_audience_objects_on(
281 &tx,
282 self.store_dir,
283 &stage.write_id,
284 &stage.audiences.packages,
285 &stage.audiences.blobs,
286 )?;
287 let head_ref = coven_protocol::store_commit::StoreDeviceHeadRef {
288 head_hash: stage.head.value.head_hash(),
289 object: stage.head.prepared.reference().clone(),
290 };
291 let head_remote = RemoteObjectRecord::candidate_activated_store_head(
292 head_ref,
293 &stage.head.value.to_bytes(),
294 stage.head.prepared.stored_bytes(),
295 commit_ref.clone(),
296 )
297 .map_err(|error| DbError::context("prepared Store head", error))?;
298 persist_exact_remote_object_on(&tx, self.store_dir, &head_remote, "Store head")?;
299
300 let prepared = PreparedStoreWriteState::Publication {
301 commit: DurablePreparedProtocolObject::new(
302 stage.commit.value.to_bytes(),
303 stage.commit.prepared,
304 ),
305 head: DurablePreparedProtocolObject::new(
306 stage.head.value.to_bytes(),
307 stage.head.prepared,
308 ),
309 history_evidence: stage.history_evidence,
310 local_cleanup: stage.local_cleanup,
311 completion: stage.completion,
312 };
313 let prepared = serde_json::to_string(&prepared)
314 .map_err(|error| DbError::context("serialize prepared Store write", error))?;
315 let status = serde_json::to_string(&WriteStatus::Publishing)
316 .map_err(|error| DbError::context("serialize write status", error))?;
317 let updated = tx
318 .execute(
319 "UPDATE store_writes SET prepared = ?2, status = ?3
320 WHERE write_id = ?1 AND prepared IS NULL AND status = '\"pending\"'",
321 rusqlite::params![stage.write_id.as_str(), prepared, status],
322 )
323 .map_err(DbError::from)?;
324 if updated != 1 {
325 return Err(DbError::Message(format!(
326 "write {} lost pending preparation ownership",
327 stage.write_id
328 )));
329 }
330 tx.commit().map_err(DbError::from)?;
331 Ok(())
332 }
333
334 fn prepare_merge_candidate_abandonment(
335 &mut self,
336 stage: MergeCandidateAbandonmentPreparation,
337 ) -> Result<(), DbError> {
338 let verified_authority = &mut *self.verified_store_authority;
339 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
340 let (raw_status, raw_prepared): (String, String) = tx
341 .query_row(
342 "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
343 [stage.write_id.as_str()],
344 |row| Ok((row.get(0)?, row.get(1)?)),
345 )
346 .map_err(DbError::from)?;
347 let status: WriteStatus = serde_json::from_str(&raw_status)
348 .map_err(|error| DbError::context("Merge abandonment status", error))?;
349 if !matches!(status, WriteStatus::Blocked(_)) {
350 return Err(DbError::Message(format!(
351 "write {} is not blocked",
352 stage.write_id
353 )));
354 }
355 let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
356 .map_err(|error| DbError::context("prepared Merge candidate", error))?;
357 let PreparedStoreWriteState::Publication {
358 commit: candidate_commit,
359 head: candidate_head,
360 history_evidence: candidate_history_evidence,
361 local_cleanup,
362 completion,
363 } = prepared
364 else {
365 return Err(DbError::Message(
366 "Merge abandonment requires one prepared candidate".to_string(),
367 ));
368 };
369 let store_transaction =
370 crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
371 let candidate = store_transaction.prepared_merge_candidate_parts(
372 verified_authority,
373 candidate_commit.semantic_bytes(),
374 candidate_commit.prepared().reference(),
375 candidate_head.semantic_bytes(),
376 candidate_head.prepared().reference(),
377 )?;
378 if candidate.commit.write_id != stage.write_id {
379 return Err(DbError::Message(
380 "prepared Merge candidate differs from its write identity".to_string(),
381 ));
382 }
383 let root = store_transaction.required_root_authority(verified_authority)?;
384 let registration = store_transaction.activated_registration(
385 verified_authority,
386 &root,
387 &candidate.commit.author_registration,
388 )?;
389 if stage.commit.value.store_root_hash() != root.store_root_hash
390 || stage.commit.value.author() != ®istration
391 || stage.commit.value.reference().coord != candidate.reference.coord
392 || stage.commit.value.reference().object != *stage.commit.prepared.reference()
393 {
394 return Err(DbError::Message(
395 "authenticated Merge abandonment commit differs from its current local authority"
396 .to_string(),
397 ));
398 }
399 if stage.commit.value.write_id != stage.write_id
400 || stage.commit.value.abandoned_candidates()
401 != [coven_protocol::store_commit::CandidateCleanupManifest {
402 candidate: coven_protocol::store_commit::StoreBatchCommitDeletionTarget {
403 coord: candidate.reference.coord.clone(),
404 object: candidate.reference.object.clone(),
405 canonical_signed_bytes: candidate.canonical_signed_bytes.clone(),
406 },
407 }]
408 {
409 return Err(DbError::Message(
410 "Merge abandonment does not name its exact prepared candidate".to_string(),
411 ));
412 }
413 let authority_ref = stage.commit.value.reference().clone();
414 if stage.head.value.commit != authority_ref
415 || stage.head.prepared.reference().slot() != candidate.head_object.slot()
416 || stage.head.value.successor != candidate.head.successor
417 || stage.head.value.author_registration != candidate.commit.author_registration
418 {
419 return Err(DbError::Message(
420 "Merge abandonment head differs from the candidate competition point".to_string(),
421 ));
422 }
423 stage
424 .history_evidence
425 .validate_for(&authority_ref, stage.commit.value.value())
426 .map_err(|error| DbError::context("Merge abandonment history evidence", error))?;
427 StoreDeviceHead::parse_at(
428 &stage.head.value.to_bytes(),
429 root.store_root_hash,
430 ®istration,
431 &authority_ref,
432 )
433 .map_err(|error| DbError::context("verify Merge abandonment head", error))?;
434 let authority_commit = RemoteObjectRecord::candidate_commit(
435 authority_ref.clone(),
436 &stage.commit.value.to_bytes(),
437 stage.commit.prepared.stored_bytes(),
438 )
439 .map_err(|error| DbError::context("Merge abandonment commit", error))?;
440 persist_exact_remote_object_on(
441 &tx,
442 self.store_dir,
443 &authority_commit,
444 "Merge abandonment commit",
445 )?;
446 let authority_head_ref = coven_protocol::store_commit::StoreDeviceHeadRef {
447 head_hash: stage.head.value.head_hash(),
448 object: stage.head.prepared.reference().clone(),
449 };
450 let authority_head = RemoteObjectRecord::candidate_activated_store_head(
451 authority_head_ref,
452 &stage.head.value.to_bytes(),
453 stage.head.prepared.stored_bytes(),
454 authority_ref,
455 )
456 .map_err(|error| DbError::context("Merge abandonment head", error))?;
457 persist_exact_remote_object_on(
458 &tx,
459 self.store_dir,
460 &authority_head,
461 "Merge abandonment head",
462 )?;
463 let replacement = PreparedStoreWriteState::MergeAbandonment {
464 candidate_commit,
465 candidate_head,
466 candidate_history_evidence,
467 authority_commit: DurablePreparedProtocolObject::new(
468 stage.commit.value.to_bytes(),
469 stage.commit.prepared,
470 ),
471 authority_head: DurablePreparedProtocolObject::new(
472 stage.head.value.to_bytes(),
473 stage.head.prepared,
474 ),
475 authority_history_evidence: stage.history_evidence,
476 outcome: MergeAbandonmentOutcome::Prepared,
477 local_cleanup,
478 completion,
479 };
480 let replacement = serde_json::to_string(&replacement)
481 .map_err(|error| DbError::context("serialize Merge abandonment", error))?;
482 let publishing = serde_json::to_string(&WriteStatus::Publishing)
483 .map_err(|error| DbError::context("serialize Merge abandonment status", error))?;
484 let updated = tx
485 .execute(
486 "UPDATE store_writes SET prepared = ?2, status = ?3
487 WHERE write_id = ?1 AND prepared = ?4
488 AND json_extract(status, '$.blocked') IS NOT NULL",
489 rusqlite::params![
490 stage.write_id.as_str(),
491 replacement,
492 publishing,
493 raw_prepared
494 ],
495 )
496 .map_err(DbError::from)?;
497 if updated != 1 {
498 return Err(DbError::Message(
499 "blocked Merge candidate changed during abandonment preparation".to_string(),
500 ));
501 }
502 tx.commit().map_err(DbError::from)?;
503 Ok(())
504 }
505
506 #[cfg(any(test, feature = "test-utils"))]
507 fn enqueue_store_changeset_for_test(
508 &mut self,
509 write_id: coven_protocol::write::WriteId,
510 changeset: Vec<u8>,
511 ) -> Result<(), DbError> {
512 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
513 let base = StoreWriteBase {
514 dependencies: crate::store::materialized_commit_index::materialized_frontier_on(
515 &tx, None,
516 )?,
517 };
518 let partitions = vec![crate::AudiencePartition {
519 audience: coven_protocol::circle::Audience::Store,
520 control: None,
521 changeset: changeset.clone(),
522 }];
523 let blob_facts = super::host_write_capture::capture_partition_blob_facts_on(
524 &tx,
525 &partitions,
526 self.blob_decls,
527 )?;
528 let changeset_hash =
529 crate::payload_store::write_payload_blocking(&tx, self.store_dir, &changeset)?;
530 crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
531 .insert_store_write(&write_id, &partitions, changeset_hash, &base, &blob_facts)?;
532 tx.commit().map_err(DbError::from)
533 }
534}
535
536impl StoreDatabase {
537 pub async fn table_schema_for_apply(&self) -> Result<crate::TableSchema, DbError> {
538 self.call_store(|session| session.table_schema_for_apply())
539 .await
540 }
541
542 pub async fn prepare_store_write_commit(
543 &self,
544 stage: StoreWritePreparation,
545 ) -> Result<(), DbError> {
546 let write_id = stage.write_id.clone();
547 self.call_store(move |session| session.prepare_store_write_commit(stage))
548 .await?;
549 self.notify_write_status(write_id, WriteStatus::Publishing);
550 Ok(())
551 }
552
553 pub async fn prepare_merge_candidate_abandonment(
554 &self,
555 stage: MergeCandidateAbandonmentPreparation,
556 ) -> Result<(), DbError> {
557 let notified_write_id = stage.write_id.clone();
558 self.call_store(move |session| session.prepare_merge_candidate_abandonment(stage))
559 .await?;
560 self.notify_write_status(notified_write_id, WriteStatus::Publishing);
561 Ok(())
562 }
563
564 #[cfg(any(test, feature = "test-utils"))]
565 pub async fn enqueue_store_changeset_for_test(
566 &self,
567 changeset: Vec<u8>,
568 ) -> Result<(), DbError> {
569 let write_id = self.new_store_write_id();
570 self.call_store(move |session| {
571 session.enqueue_store_changeset_for_test(write_id, changeset)
572 })
573 .await
574 }
575}