1use std::collections::{BTreeMap, BTreeSet, HashMap};
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14
15use rusqlite::{Connection, OptionalExtension};
16use tracing::error;
17use tracing::warn;
18
19use crate::blob::decl::BlobDecls;
20use crate::blob::locator::{BlobLocator, RemoteAudience, StoredBlobRef};
21use crate::blob::{BlobRef, Provenance, RowBlobAuthority, RowBlobRef};
22use crate::db::{
23 apply_coven_schema, expected_coven_schema_manifest, is_reserved_table_name,
24 live_coven_schema_manifest, CovenSchemaManifest, ExternalBlob, OutboxEntry, OutboxOperation,
25 OutboxUploadState,
26};
27use crate::encryption::EncryptionService;
28use crate::migration::{run_migrations_in_transaction, Migration, MigrationError};
29use crate::sync::audience_package::{AudiencePackage, RowBlobLocatorBinding};
30use crate::sync::circle::Audience;
31use crate::sync::gate::{self, Gates};
32use crate::sync::hlc::{Hlc, Timestamp, UpdatedAtStamper, HIGHWATER_STATE_KEY, MAX_FUTURE_SKEW_MS};
33use crate::sync::membership::{
34 AuthorHead, AuthorStreamId, MembershipEntry, MembershipEntryRef, MembershipHeadRef,
35 SerialAuthorizationState, SerialMembershipState,
36};
37use crate::sync::provider::ProviderAdminState;
38use crate::sync::remote_object::{
39 remote_object_id, CandidateExclusiveObjectDomain, RemoteObjectRecord, SharedLiveSetObjectDomain,
40};
41use crate::sync::routing_contract::SyncRoutingContract;
42use crate::sync::session::{quote_ident, SyncedTable};
43use crate::sync::storage::{ExactObjectRef, PreparedExactObject, VersionToken, VersionedObject};
44use crate::sync::store_commit::{
45 ack_slot_prefix, commit_semantic_prefix, snapshot_image_semantic_prefix, snapshot_slot_prefix,
46 CommitFrontier, ObjectHash, ResolvedStoreDeviceState, SnapshotImageRef, SnapshotMeta, StoreAck,
47 StoreAckRef, StoreBatchCommit, StoreBatchCommitRef, StoreCommitCoord, StoreDeviceHead,
48 StoreDeviceRegistration, StoreDeviceRegistrationRef, StoreDeviceStateRef, StoreProtocolRoot,
49 StoreSerialHead, StoreSerialHeadState, StoreSerialPredecessor, StoreSnapshotRef,
50 StreamActivationId, SERIAL_STREAM_ID,
51};
52use crate::write::{
53 AffectedRow, PendingBranch, PendingBranchId, PendingWrite, PublishedPosition, WriteId,
54 WriteReceipt, WriteResolution, WriteStatus,
55};
56use crate::WritePolicy;
57
58pub const LOCAL_DEVICE_ID_STATE_KEY: &str = "local_device_id";
59const HOST_DEVICE_ID_STATE_KEY: &str = "host_device_id";
60pub const WRITE_POLICY_STATE_KEY: &str = "write_policy";
61const SYNC_ROUTING_CONTRACT_STATE_KEY: &str = "sync_routing_contract";
62pub const SYNC_ROUTING_HASH_STATE_KEY: &str = "sync_routing_hash";
63const COVEN_SCHEMA_MANIFEST_STATE_KEY: &str = "coven_schema_manifest";
64const COVEN_INITIALIZED_STATE_KEY: &str = "coven_initialized";
65const COVEN_INITIALIZED_STATE_VALUE: &str = "1";
66pub const SERIAL_MEMBERSHIP_STATE_KEY: &str = "serial_membership_state";
67pub const SERIAL_KEY_GENERATION_STATE_KEY: &str = "serial_key_generation";
68pub const SERIAL_PROVIDER_ADMIN_STATE_KEY: &str = "serial_provider_admin_state";
69const SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY: &str = "serial_candidate_abandonment";
70const STORE_DEVICE_GENESIS_STATE_KEY: &str = "store_device_genesis_state";
71const GATE_BASELINE_SCHEMA: &str = "coven_gate_empty";
72
73fn load_store_device_genesis_state_on(
74 conn: &Connection,
75) -> Result<ResolvedStoreDeviceState, DbError> {
76 let raw: String = conn
77 .query_row(
78 "SELECT value FROM protocol_state WHERE key = ?1",
79 [STORE_DEVICE_GENESIS_STATE_KEY],
80 |row| row.get(0),
81 )
82 .map_err(DbError::from)?;
83 serde_json::from_str(&raw)
84 .map_err(|error| DbError::Message(format!("parse Store device genesis state: {error}")))
85}
86
87fn store_serial_predecessor_on(
88 conn: &Connection,
89 commit: Option<&StoreBatchCommitRef>,
90) -> Result<StoreSerialPredecessor, DbError> {
91 if let Some(commit) = commit {
92 return Ok(StoreSerialPredecessor::Commit(commit.clone()));
93 }
94 let root = required_store_root_authority_on(conn)?;
95 let genesis = load_store_device_genesis_state_on(conn)?;
96 let mut devices = genesis.devices.values();
97 let Some(founder) = devices.next() else {
98 return Err(DbError::Message(
99 "Serial genesis must contain exactly one founder registration".to_string(),
100 ));
101 };
102 if devices.next().is_some() {
103 return Err(DbError::Message(
104 "Serial genesis must contain exactly one founder registration".to_string(),
105 ));
106 }
107 Ok(StoreSerialPredecessor::Genesis {
108 root,
109 founder_registration: founder.registration.clone(),
110 })
111}
112
113fn load_store_device_snapshot_on(
114 conn: &Connection,
115 reference: &StoreBatchCommitRef,
116) -> Result<ResolvedStoreDeviceState, DbError> {
117 let exact = serde_json::to_string(reference)
118 .map_err(|error| DbError::Message(format!("serialize Store commit ref: {error}")))?;
119 let raw: String = conn
120 .query_row(
121 "SELECT state FROM store_device_state_snapshots WHERE commit_ref = ?1",
122 [exact],
123 |row| row.get(0),
124 )
125 .map_err(DbError::from)?;
126 serde_json::from_str(&raw)
127 .map_err(|error| DbError::Message(format!("parse Store device state snapshot: {error}")))
128}
129
130fn load_declared_store_device_state_on(
131 conn: &Connection,
132 reference: &StoreDeviceStateRef,
133) -> Result<ResolvedStoreDeviceState, DbError> {
134 let state = match reference {
135 StoreDeviceStateRef::MergeConcurrent { frontier, .. } => {
136 let CommitFrontier::MergeConcurrent(frontier) = frontier else {
137 return Err(DbError::Message(
138 "Merge device state carries a Serial frontier".into(),
139 ));
140 };
141 if frontier.is_empty() {
142 load_store_device_genesis_state_on(conn)?
143 } else {
144 ResolvedStoreDeviceState::merge(
145 frontier
146 .values()
147 .map(|commit| load_store_device_snapshot_on(conn, commit))
148 .collect::<Result<Vec<_>, _>>()?,
149 )
150 .map_err(|error| DbError::Message(error.to_string()))?
151 }
152 }
153 StoreDeviceStateRef::Serial { position, .. } => match position {
154 StoreSerialPredecessor::Genesis { .. } => load_store_device_genesis_state_on(conn)?,
155 StoreSerialPredecessor::Commit(commit) => load_store_device_snapshot_on(conn, commit)?,
156 },
157 };
158 if state.state_hash != reference.state_hash() || state.recovery != reference.recovery() {
159 return Err(DbError::Message(
160 "declared Store device state differs from its exact predecessor snapshots".into(),
161 ));
162 }
163 Ok(state)
164}
165
166fn authorize_host_sql(context: rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization {
167 use rusqlite::hooks::{AuthAction, Authorization};
168
169 if context
170 .database_name
171 .is_some_and(|name| name.eq_ignore_ascii_case(GATE_BASELINE_SCHEMA))
172 || matches!(
173 context.action,
174 AuthAction::Detach { database_name }
175 if database_name.eq_ignore_ascii_case(GATE_BASELINE_SCHEMA)
176 )
177 || matches!(
178 context.action,
179 AuthAction::Pragma { pragma_name, .. }
180 if pragma_name.eq_ignore_ascii_case("database_list")
181 )
182 {
183 Authorization::Deny
184 } else {
185 Authorization::Allow
186 }
187}
188
189#[derive(Debug, thiserror::Error)]
191pub enum DbError {
192 #[error("database error: {0}")]
193 Message(String),
194 #[error("database error: Store protocol root hash is absent")]
195 StoreRootHashMissing,
196}
197
198impl DbError {
199 pub(crate) fn into_message(self) -> String {
200 match self {
201 Self::Message(message) => message,
202 Self::StoreRootHashMissing => "Store protocol root hash is absent".to_string(),
203 }
204 }
205
206 fn missing_store_root_hash() -> Self {
207 Self::StoreRootHashMissing
208 }
209}
210
211impl From<rusqlite::Error> for DbError {
212 fn from(e: rusqlite::Error) -> Self {
213 DbError::Message(e.to_string())
214 }
215}
216
217#[derive(Debug, thiserror::Error)]
223pub enum OpenError {
224 #[error(transparent)]
225 Migration(#[from] MigrationError),
226 #[error(transparent)]
227 Db(#[from] DbError),
228}
229
230#[derive(Clone)]
231struct DatabaseState {
232 write_policy: WritePolicy,
233 hlc: Arc<Hlc>,
234 synced_tables: Arc<Vec<SyncedTable>>,
235 schema_version: u32,
236 sync_routing_hash: ObjectHash,
237 gates: Arc<Gates>,
238 blob_decls: Arc<BlobDecls>,
239 blob_tombstone_grace: chrono::Duration,
244 transfer_limits: crate::blob::TransferLimits,
248 membership_load: Arc<tokio::sync::Mutex<()>>,
251 membership_mutation: Arc<tokio::sync::Mutex<()>>,
254 snapshot_publication: Arc<tokio::sync::Mutex<()>>,
257 local_blob_cleanup: Arc<tokio::sync::Mutex<()>>,
260 ids: crate::id_provider::IdRef,
261 write_statuses:
262 Arc<std::sync::Mutex<HashMap<WriteId, tokio::sync::watch::Sender<WriteStatus>>>>,
263 #[cfg(any(test, feature = "test-utils"))]
264 test_pause_points: Arc<TestPausePoints<DatabaseTestPoint>>,
265}
266
267#[cfg(any(test, feature = "test-utils"))]
269#[doc(hidden)]
270#[derive(Clone, Debug, PartialEq, Eq)]
271pub enum DatabaseTestPoint {
272 LocalBlobCleanupRequested,
273 LocalBlobCleanupAcquired,
274 LocalBlobCleanupBeforeFilesystem { namespace: String, blob_id: String },
275 LocalBlobCleanupFinished,
276 PullAfterRemoteCommit { device_id: String, seq: u64 },
277}
278
279#[cfg(any(test, feature = "test-utils"))]
280struct ArmedTestPause<K> {
281 point: K,
282 reached: Arc<tokio::sync::Notify>,
283 resume: Arc<tokio::sync::Notify>,
284}
285
286#[cfg(any(test, feature = "test-utils"))]
287struct TestPauseState<K> {
288 armed: Option<ArmedTestPause<K>>,
289 observers: Vec<tokio::sync::mpsc::UnboundedSender<K>>,
290}
291
292#[cfg(any(test, feature = "test-utils"))]
293struct TestPausePoints<K> {
294 state: std::sync::Mutex<TestPauseState<K>>,
295}
296
297#[cfg(any(test, feature = "test-utils"))]
298impl<K> Default for TestPausePoints<K> {
299 fn default() -> Self {
300 Self {
301 state: std::sync::Mutex::new(TestPauseState {
302 armed: None,
303 observers: Vec::new(),
304 }),
305 }
306 }
307}
308
309#[cfg(any(test, feature = "test-utils"))]
310impl<K: Clone + PartialEq> TestPausePoints<K> {
311 fn arm(&self, point: K) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
312 let reached = Arc::new(tokio::sync::Notify::new());
313 let resume = Arc::new(tokio::sync::Notify::new());
314 let prior = self
315 .state
316 .lock()
317 .expect("database test pause mutex poisoned")
318 .armed
319 .replace(ArmedTestPause {
320 point,
321 reached: reached.clone(),
322 resume: resume.clone(),
323 });
324 assert!(prior.is_none(), "database test pause already armed");
325 (reached, resume)
326 }
327
328 fn observe(&self) -> tokio::sync::mpsc::UnboundedReceiver<K> {
329 let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
330 self.state
331 .lock()
332 .expect("database test pause mutex poisoned")
333 .observers
334 .push(sender);
335 receiver
336 }
337
338 async fn reach(&self, point: K) {
339 let pause = {
340 let mut state = self
341 .state
342 .lock()
343 .expect("database test pause mutex poisoned");
344 state
345 .observers
346 .retain(|observer| observer.send(point.clone()).is_ok());
347 if state
348 .armed
349 .as_ref()
350 .is_some_and(|pause| pause.point == point)
351 {
352 state.armed.take()
353 } else {
354 None
355 }
356 };
357 if let Some(pause) = pause {
358 pause.reached.notify_one();
359 pause.resume.notified().await;
360 }
361 }
362}
363
364struct DatabaseCore {
376 conn: Connection,
377 hlc: Arc<Hlc>,
378 synced_tables: Arc<Vec<SyncedTable>>,
379 schema_version: u32,
380 sync_routing_hash: ObjectHash,
381 gates: Arc<Gates>,
382 blob_decls: Arc<BlobDecls>,
383 blob_tombstone_grace: chrono::Duration,
384 transfer_limits: crate::blob::TransferLimits,
385 write_policy: WritePolicy,
386}
387
388#[derive(Clone, Copy)]
389enum CovenMetadataOpen {
390 Detect,
391 InitializeVerifiedSnapshot,
392}
393
394fn serialized_write_policy(write_policy: WritePolicy) -> Result<String, DbError> {
395 serde_json::to_string(&write_policy)
396 .map_err(|error| DbError::Message(format!("serialize Store write policy: {error}")))
397}
398
399fn initialize_write_policy(conn: &Connection, write_policy: WritePolicy) -> Result<(), DbError> {
400 let serialized = serialized_write_policy(write_policy)?;
401 conn.execute(
402 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
403 (WRITE_POLICY_STATE_KEY, serialized),
404 )
405 .map_err(DbError::from)?;
406 Ok(())
407}
408
409fn protocol_state_exists(conn: &Connection) -> Result<bool, DbError> {
410 conn.query_row(
411 "SELECT EXISTS(\
412 SELECT 1 FROM main.sqlite_schema \
413 WHERE type = 'table' AND name = 'protocol_state'\
414 )",
415 [],
416 |row| row.get(0),
417 )
418 .map_err(DbError::from)
419}
420
421fn has_coven_initialization_marker(conn: &Connection) -> Result<bool, DbError> {
422 if !protocol_state_exists(conn)? {
423 return Ok(false);
424 }
425 let marker = conn
426 .query_row(
427 "SELECT value FROM protocol_state WHERE key = ?1",
428 [COVEN_INITIALIZED_STATE_KEY],
429 |row| row.get::<_, String>(0),
430 )
431 .optional()
432 .map_err(DbError::from)?;
433 match marker.as_deref() {
434 None => Ok(false),
435 Some(COVEN_INITIALIZED_STATE_VALUE) => Ok(true),
436 Some(value) => Err(DbError::Message(format!(
437 "Store database has invalid Coven initialization marker {value:?}"
438 ))),
439 }
440}
441
442fn initialize_coven_metadata_on(
443 conn: &Connection,
444 write_policy: WritePolicy,
445 sync_routing_contract: &SyncRoutingContract,
446 install_routing_schema: bool,
447) -> Result<(), DbError> {
448 apply_coven_schema(conn).map_err(DbError::from)?;
449 if install_routing_schema {
450 crate::db::apply_coven_routing_schema(conn).map_err(DbError::from)?;
451 }
452 let schema_manifest = validate_live_coven_schema(conn, install_routing_schema)?;
453 initialize_write_policy(conn, write_policy)?;
454 let contract_json =
455 String::from_utf8(sync_routing_contract.bytes().to_vec()).map_err(|error| {
456 DbError::Message(format!("encode sync-routing contract metadata: {error}"))
457 })?;
458 conn.execute(
459 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
460 (SYNC_ROUTING_CONTRACT_STATE_KEY, contract_json),
461 )
462 .map_err(DbError::from)?;
463 conn.execute(
464 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
465 (COVEN_SCHEMA_MANIFEST_STATE_KEY, schema_manifest),
466 )
467 .map_err(DbError::from)?;
468 conn.execute(
469 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
470 (
471 SYNC_ROUTING_HASH_STATE_KEY,
472 sync_routing_contract.hash().to_string(),
473 ),
474 )
475 .map_err(DbError::from)?;
476 conn.execute(
477 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
478 (COVEN_INITIALIZED_STATE_KEY, COVEN_INITIALIZED_STATE_VALUE),
479 )
480 .map(|_| ())
481 .map_err(DbError::from)
482}
483
484fn validate_live_coven_schema(conn: &Connection, include_routing: bool) -> Result<String, DbError> {
485 let expected = expected_coven_schema_manifest(include_routing).map_err(DbError::from)?;
486 let actual = live_coven_schema_manifest(conn).map_err(DbError::from)?;
487 if actual != expected {
488 return Err(DbError::Message(format!(
489 "Coven schema does not match the current table, index, constraint, primary-key, STRICT, and WITHOUT ROWID declarations: expected {expected:?}, found {actual:?}"
490 )));
491 }
492 serde_json::to_string(&expected)
493 .map_err(|error| DbError::Message(format!("serialize Coven schema manifest: {error}")))
494}
495
496fn validate_initialized_coven_schema(
497 conn: &Connection,
498 include_routing: bool,
499) -> Result<(), DbError> {
500 let expected = expected_coven_schema_manifest(include_routing).map_err(DbError::from)?;
501 let stored_json = conn
502 .query_row(
503 "SELECT value FROM protocol_state WHERE key = ?1",
504 [COVEN_SCHEMA_MANIFEST_STATE_KEY],
505 |row| row.get::<_, String>(0),
506 )
507 .optional()
508 .map_err(DbError::from)?
509 .ok_or_else(|| {
510 DbError::Message(
511 "Store database is missing required Coven schema manifest metadata".to_string(),
512 )
513 })?;
514 let stored: CovenSchemaManifest = serde_json::from_str(&stored_json).map_err(|error| {
515 DbError::Message(format!("Store Coven schema manifest is invalid: {error}"))
516 })?;
517 if stored != expected {
518 return Err(DbError::Message(format!(
519 "Store Coven schema manifest does not match the current schema: stored {stored:?}, current {expected:?}"
520 )));
521 }
522 validate_live_coven_schema(conn, include_routing)?;
523 Ok(())
524}
525
526fn load_coven_metadata(
527 conn: &Connection,
528 write_policy: WritePolicy,
529) -> Result<SyncRoutingContract, DbError> {
530 if !has_coven_initialization_marker(conn)? {
531 return Err(DbError::Message(
532 "Store database is missing required Coven initialization metadata".to_string(),
533 ));
534 }
535 validate_write_policy(conn, write_policy)?;
536 let contract_bytes = conn
537 .query_row(
538 "SELECT value FROM protocol_state WHERE key = ?1",
539 [SYNC_ROUTING_CONTRACT_STATE_KEY],
540 |row| row.get::<_, String>(0),
541 )
542 .optional()
543 .map_err(DbError::from)?
544 .ok_or_else(|| {
545 DbError::Message(
546 "Store database is missing required sync_routing_contract metadata".to_string(),
547 )
548 })?;
549 let contract = SyncRoutingContract::from_bytes(contract_bytes.as_bytes()).map_err(|error| {
550 DbError::Message(format!("Store sync-routing contract is invalid: {error}"))
551 })?;
552 let stored_hash = conn
553 .query_row(
554 "SELECT value FROM protocol_state WHERE key = ?1",
555 [SYNC_ROUTING_HASH_STATE_KEY],
556 |row| row.get::<_, String>(0),
557 )
558 .optional()
559 .map_err(DbError::from)?
560 .ok_or_else(|| {
561 DbError::Message(
562 "Store database is missing required sync_routing_hash metadata".to_string(),
563 )
564 })?;
565 let stored_hash: ObjectHash = stored_hash.parse().map_err(|error| {
566 DbError::Message(format!(
567 "Store sync_routing_hash metadata is invalid: {error}"
568 ))
569 })?;
570 if stored_hash != contract.hash() {
571 return Err(DbError::Message(format!(
572 "Store sync-routing contract hashes to {}, but metadata records {stored_hash}",
573 contract.hash(),
574 )));
575 }
576 Ok(contract)
577}
578
579fn validate_sync_routing_contract(
580 pinned: &SyncRoutingContract,
581 resolved: &SyncRoutingContract,
582) -> Result<(), DbError> {
583 if pinned.bytes() != resolved.bytes() || pinned.hash() != resolved.hash() {
584 return Err(DbError::Message(format!(
585 "Store sync-routing hash is {}, but open resolved {}",
586 pinned.hash(),
587 resolved.hash(),
588 )));
589 }
590 Ok(())
591}
592
593fn validate_write_policy(conn: &Connection, requested: WritePolicy) -> Result<(), DbError> {
594 let stored = conn
595 .query_row(
596 "SELECT value FROM protocol_state WHERE key = ?1",
597 [WRITE_POLICY_STATE_KEY],
598 |row| row.get::<_, String>(0),
599 )
600 .optional()
601 .map_err(DbError::from)?
602 .ok_or_else(|| {
603 DbError::Message("Store database is missing required write_policy metadata".to_string())
604 })?;
605 let stored: WritePolicy = serde_json::from_str(&stored).map_err(|error| {
606 DbError::Message(format!("Store write_policy metadata is invalid: {error}"))
607 })?;
608 if stored != requested {
609 return Err(DbError::Message(format!(
610 "Store write policy is {stored:?}, but open requested {requested:?}"
611 )));
612 }
613 Ok(())
614}
615
616impl DatabaseCore {
617 fn open(
618 path: &Path,
619 synced_tables: Vec<SyncedTable>,
620 blob_tombstone_grace: chrono::Duration,
621 transfer_limits: crate::blob::TransferLimits,
622 write_policy: WritePolicy,
623 hlc: Arc<Hlc>,
624 migrations: &[Migration],
625 metadata_open: CovenMetadataOpen,
626 ) -> Result<(Self, DatabaseState, UpdatedAtStamper), OpenError> {
627 let mut conn = open_connection(path)?;
628 conn.pragma_update(None, "foreign_keys", "ON")
629 .map_err(DbError::from)?;
630
631 let initialized = match metadata_open {
632 CovenMetadataOpen::InitializeVerifiedSnapshot => false,
633 CovenMetadataOpen::Detect => {
634 let initialized = has_coven_initialization_marker(&conn)?;
635 if !initialized
636 && !live_coven_schema_manifest(&conn)
637 .map_err(DbError::from)?
638 .is_empty()
639 {
640 return Err(DbError::Message(
641 "Store database contains Coven schema objects without the required initialization marker"
642 .to_string(),
643 )
644 .into());
645 }
646 initialized
647 }
648 };
649 let pinned_routing_contract = initialized
650 .then(|| load_coven_metadata(&conn, write_policy))
651 .transpose()?;
652 if let Some(pinned) = &pinned_routing_contract {
653 validate_initialized_coven_schema(
654 &conn,
655 write_policy == WritePolicy::MergeConcurrent && pinned.has_scoped_graph(),
656 )?;
657 }
658 let (schema_version, sync_routing_contract, gates, blob_decls) = {
659 let tx = conn.transaction().map_err(DbError::from)?;
660 let outcome = (|| -> Result<_, OpenError> {
661 let schema_version = run_migrations_in_transaction(&tx, migrations)?;
662
663 validate_host_synced_tables(&tx, &synced_tables)?;
667 let resolved = SyncRoutingContract::from_connection(&tx, &synced_tables)
668 .map_err(|error| DbError::Message(error.to_string()))?;
669 if let Some(pinned) = &pinned_routing_contract {
670 validate_sync_routing_contract(pinned, &resolved)?;
671 validate_initialized_coven_schema(
672 &tx,
673 write_policy == WritePolicy::MergeConcurrent && resolved.has_scoped_graph(),
674 )?;
675 } else {
676 initialize_coven_metadata_on(
677 &tx,
678 write_policy,
679 &resolved,
680 write_policy == WritePolicy::MergeConcurrent && resolved.has_scoped_graph(),
681 )?;
682 }
683 pin_host_device_id_on(&tx, hlc.device_id(), initialized)?;
684 let gates = Gates::from_tables(&tx, &synced_tables)
685 .map_err(|error| DbError::Message(error.to_string()))?;
686 let blob_decls = BlobDecls::from_tables(&tx, &synced_tables)
687 .map_err(|error| DbError::Message(error.to_string()))?;
688 Ok((schema_version, resolved, gates, blob_decls))
689 })();
690 match outcome {
691 Ok(initialized) => {
692 tx.commit().map_err(DbError::from)?;
693 initialized
694 }
695 Err(error) => return Err(error),
696 }
697 };
698 let sync_routing_hash = sync_routing_contract.hash();
699 let persisted = conn
703 .query_row(
704 "SELECT value FROM protocol_state WHERE key = ?1",
705 [HIGHWATER_STATE_KEY],
706 |r| r.get::<_, String>(0),
707 )
708 .optional()
709 .map_err(DbError::from)?;
710 seed_from(&hlc, persisted, "HLC high-water mark in protocol_state")?;
711 let seed_wall_ms = hlc.wall_now_ms();
712 let seed_bound_ms = seed_wall_ms.saturating_add(MAX_FUTURE_SKEW_MS);
713 let on_disk = scan_max_updated_at(&conn, &synced_tables, seed_bound_ms)?;
714 seed_from(&hlc, on_disk, "`_updated_at` in synced tables")?;
715
716 let stamper = UpdatedAtStamper::new(hlc.clone());
717 let synced_tables = Arc::new(synced_tables);
718 let gates = Arc::new(gates);
719 let blob_decls = Arc::new(blob_decls);
720 blob_decls
721 .install_cleanup_guards(&conn)
722 .map_err(|e| DbError::Message(e.to_string()))?;
723 gate::attach_empty_clone(&conn, &gates)
724 .map_err(|error| DbError::Message(format!("install host transaction gate: {error}")))?;
725 let core = DatabaseCore {
726 conn,
727 hlc,
728 synced_tables,
729 schema_version,
730 sync_routing_hash,
731 gates,
732 blob_decls,
733 blob_tombstone_grace,
734 transfer_limits,
735 write_policy,
736 };
737 let state = core.state();
738
739 Ok((core, state, stamper))
740 }
741
742 fn open_read_only(
749 path: &Path,
750 synced_tables: Vec<SyncedTable>,
751 blob_tombstone_grace: chrono::Duration,
752 transfer_limits: crate::blob::TransferLimits,
753 write_policy: WritePolicy,
754 hlc: Arc<Hlc>,
755 migrations: &[Migration],
756 ) -> Result<(Self, DatabaseState), OpenError> {
757 let conn = open_connection_read_only(path)?;
758 conn.pragma_update(None, "foreign_keys", "ON")
762 .map_err(DbError::from)?;
763 let schema_version = crate::migration::ensure_schema_supported(&conn, migrations)?;
768
769 validate_host_synced_tables(&conn, &synced_tables)?;
773 let pinned_routing_contract = load_coven_metadata(&conn, write_policy)?;
774 validate_initialized_coven_schema(
775 &conn,
776 write_policy == WritePolicy::MergeConcurrent
777 && pinned_routing_contract.has_scoped_graph(),
778 )?;
779 validate_host_device_id_on(&conn, hlc.device_id())?;
780 let sync_routing_contract = SyncRoutingContract::from_connection(&conn, &synced_tables)
781 .map_err(|error| DbError::Message(error.to_string()))?;
782 validate_sync_routing_contract(&pinned_routing_contract, &sync_routing_contract)?;
783 let sync_routing_hash = sync_routing_contract.hash();
784
785 let synced_tables = Arc::new(synced_tables);
786
787 let gates = Arc::new(
790 Gates::from_tables(&conn, &synced_tables)
791 .map_err(|e| DbError::Message(e.to_string()))?,
792 );
793 let blob_decls = Arc::new(
794 BlobDecls::from_tables(&conn, &synced_tables)
795 .map_err(|e| DbError::Message(e.to_string()))?,
796 );
797 let core = DatabaseCore {
798 conn,
799 hlc,
800 synced_tables,
801 schema_version,
802 sync_routing_hash,
803 gates,
804 blob_decls,
805 blob_tombstone_grace,
806 transfer_limits,
807 write_policy,
808 };
809 let state = core.state();
810 Ok((core, state))
811 }
812
813 fn state(&self) -> DatabaseState {
814 DatabaseState {
815 write_policy: self.write_policy,
816 hlc: self.hlc.clone(),
817 synced_tables: self.synced_tables.clone(),
818 schema_version: self.schema_version,
819 sync_routing_hash: self.sync_routing_hash,
820 gates: self.gates.clone(),
821 blob_decls: self.blob_decls.clone(),
822 blob_tombstone_grace: self.blob_tombstone_grace,
823 transfer_limits: self.transfer_limits,
824 membership_load: Arc::new(tokio::sync::Mutex::new(())),
825 membership_mutation: Arc::new(tokio::sync::Mutex::new(())),
826 snapshot_publication: Arc::new(tokio::sync::Mutex::new(())),
827 local_blob_cleanup: Arc::new(tokio::sync::Mutex::new(())),
828 ids: Arc::new(crate::id_provider::UuidProvider),
829 write_statuses: Arc::new(std::sync::Mutex::new(HashMap::new())),
830 #[cfg(any(test, feature = "test-utils"))]
831 test_pause_points: Arc::new(TestPausePoints::default()),
832 }
833 }
834
835 fn connection(&self) -> &Connection {
836 &self.conn
837 }
838}
839
840enum DbJob {
846 Run(Box<dyn FnOnce(&mut DatabaseCore) + Send>),
847 Stop,
848}
849
850struct ConnectionThread {
855 jobs: tokio::sync::mpsc::UnboundedSender<DbJob>,
856 join: Option<std::thread::JoinHandle<()>>,
857}
858
859impl Drop for ConnectionThread {
860 fn drop(&mut self) {
861 let _ = self.jobs.send(DbJob::Stop);
866 let handle = match self.join.take() {
867 Some(handle) => handle,
868 None => return,
869 };
870 if tokio::runtime::Handle::try_current().is_ok() {
871 drop(handle);
879 } else {
880 if handle.join().is_err() {
886 error!("database connection thread panicked");
887 }
888 }
889 }
890}
891
892fn run_connection_thread(
896 mut core: DatabaseCore,
897 mut jobs: tokio::sync::mpsc::UnboundedReceiver<DbJob>,
898) {
899 while let Some(job) = jobs.blocking_recv() {
900 match job {
901 DbJob::Run(f) => f(&mut core),
902 DbJob::Stop => break,
903 }
904 }
905 }
908
909#[derive(Clone)]
913pub struct Database {
914 thread: Arc<ConnectionThread>,
915 state: DatabaseState,
916}
917
918pub(crate) struct PreparedStoreWrite {
919 pub write_id: WriteId,
920 pub changeset: Vec<u8>,
921 pub partitions: PreparedStoreWritePartitions,
922 pub inverse_changeset: Vec<u8>,
923 pub base: StoreWriteBase,
924 pub blob_facts: StoreWriteBlobFacts,
925}
926
927#[derive(Debug, Clone, PartialEq, Eq)]
928pub(crate) struct PreparedStoreWritePartitions {
929 pub store: Option<gate::AudiencePartition>,
930 pub circles: Vec<gate::AudiencePartition>,
931}
932
933#[derive(Clone, Copy)]
934enum StoreWriteRouting<'a> {
935 Unscoped,
936 MergeScoped(&'a EncryptionService),
937 SerialScoped,
938}
939
940impl PreparedStoreWritePartitions {
941 #[cfg(test)]
942 pub(crate) fn iter(&self) -> impl Iterator<Item = &gate::AudiencePartition> {
943 self.store.iter().chain(self.circles.iter())
944 }
945}
946
947pub(crate) struct SerialStoreBranchPreparationWork {
948 pub branch_id: PendingBranchId,
949 pub base: Option<StoreBatchCommitRef>,
950 pub writes: Vec<PreparedStoreWrite>,
951}
952
953#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
954#[serde(rename_all = "snake_case", deny_unknown_fields)]
955pub(crate) enum StoreWriteBase {
956 MergeConcurrent {
957 dependencies: BTreeMap<String, StoreBatchCommitRef>,
958 },
959 Serial {
960 branch_id: PendingBranchId,
961 base: Option<StoreBatchCommitRef>,
962 },
963}
964
965#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
966#[serde(deny_unknown_fields)]
967pub(crate) struct StoreWriteBlobFacts {
968 pub blobs: Vec<StoreWriteBlobFact>,
969}
970
971#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
972#[serde(deny_unknown_fields)]
973pub(crate) struct StoreWriteBlobFact {
974 pub table: String,
975 pub row_id: String,
976 pub row_stamp: String,
977 pub column: String,
978 pub blob: BlobRef,
979 pub plaintext_size: u64,
980 pub plaintext_hash: ObjectHash,
981 pub external_path: Option<PathBuf>,
982 pub previous: Option<StoreWriteRemoteBlob>,
983}
984
985#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
986#[serde(deny_unknown_fields)]
987pub(crate) struct StoreWriteRemoteBlob {
988 pub authority: crate::sync::audience_package::PackageAudience,
989 pub stored: StoredBlobRef,
990}
991
992impl StoreWriteBlobFact {
993 fn identity_key(&self) -> (String, String, String, String) {
994 (
995 self.table.clone(),
996 self.row_id.clone(),
997 self.column.clone(),
998 self.row_stamp.clone(),
999 )
1000 }
1001}
1002
1003#[derive(Debug, Clone)]
1004pub(crate) struct ExactProtocolObject<T> {
1005 pub value: T,
1006 pub bytes: Vec<u8>,
1007 pub object: ExactObjectRef,
1008 pub prepared: PreparedExactObject,
1009}
1010
1011#[derive(Debug, Clone)]
1012pub(crate) struct CanonicalProtocolObject<T> {
1013 pub value: T,
1014 pub bytes: Vec<u8>,
1015}
1016
1017pub(crate) struct PreparedProtocolObject<T> {
1018 pub value: T,
1019 pub prepared: PreparedExactObject,
1020}
1021
1022#[derive(Debug, Clone)]
1023pub(crate) struct PreparedStoreWriteCommit {
1024 pub audiences: PreparedAudienceObjects,
1025 pub commit: ExactProtocolObject<StoreBatchCommit>,
1026 pub head: ExactProtocolObject<StoreDeviceHead>,
1027}
1028
1029#[derive(Debug, Clone)]
1030pub(crate) struct BlockedMergeCandidate {
1031 pub commit: ExactProtocolObject<StoreBatchCommit>,
1032 pub head: ExactProtocolObject<StoreDeviceHead>,
1033}
1034
1035#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1036pub(crate) enum MergeAbandonmentState {
1037 None,
1038 Prepared,
1039 Accepted,
1040 CandidateWon,
1041 OtherWon,
1042}
1043
1044#[derive(Debug, Clone)]
1045pub(crate) struct PreparedSerialStoreWriteCommit {
1046 pub audiences: PreparedAudienceObjects,
1047 pub commit: ExactProtocolObject<StoreBatchCommit>,
1048}
1049
1050#[derive(Debug, Clone)]
1051pub(crate) struct PreparedSerialStoreBranch {
1052 pub branch_id: PendingBranchId,
1053 pub base: Option<StoreBatchCommitRef>,
1054 pub base_head_bytes: Option<Vec<u8>>,
1055 pub base_head_version: Option<VersionToken>,
1056 pub writes: Vec<PreparedSerialStoreWriteCommit>,
1057 pub head: CanonicalProtocolObject<StoreSerialHead>,
1058}
1059
1060#[derive(Debug, Clone)]
1061pub(crate) struct PreparedSerialCandidateAbandonment {
1062 pub branch_id: PendingBranchId,
1063 pub base: Option<StoreBatchCommitRef>,
1064 pub base_head: VersionedObject,
1065 pub authority: ExactProtocolObject<StoreBatchCommit>,
1066 pub head: CanonicalProtocolObject<StoreSerialHead>,
1067 pub original_head_bytes: Vec<u8>,
1068 durable_state: String,
1069}
1070
1071#[derive(Debug, Clone, PartialEq, Eq)]
1072pub enum SerialBranchDiscardState {
1073 Local,
1074 Abandonment,
1075 Conflict,
1076}
1077
1078#[derive(Debug, Clone)]
1079pub(crate) struct UnresolvedSerialBranch {
1080 pub branch_id: PendingBranchId,
1081 pub base: Option<StoreBatchCommitRef>,
1082 pub conflicted: bool,
1083}
1084
1085#[derive(Debug, Clone)]
1086pub(crate) struct OutboundStoreAck {
1087 pub reference: StoreAckRef,
1088 pub ack: ExactProtocolObject<StoreAck>,
1089}
1090
1091#[derive(Debug, Clone)]
1092pub(crate) struct PublishedStoreAck {
1093 pub reference: StoreAckRef,
1094 pub successor_slot: crate::storage::cloud::ObjectSlot,
1095}
1096
1097#[derive(Debug, Clone)]
1098pub(crate) struct DurableFounderGraph {
1099 pub root: ExactProtocolObject<StoreProtocolRoot>,
1100 pub registration: ExactProtocolObject<StoreDeviceRegistration>,
1101 pub initial_ack: ExactProtocolObject<StoreAck>,
1102 pub initial_ack_ref: StoreAckRef,
1103 pub membership: DurableFounderMembership,
1104 pub registration_state: LocalDeviceRegistrationState,
1105}
1106
1107#[derive(Debug, Clone)]
1108pub(crate) enum DurableFounderMembership {
1109 MergeConcurrent {
1110 entry: ExactProtocolObject<MembershipEntry>,
1111 entry_ref: MembershipEntryRef,
1112 head: ExactProtocolObject<AuthorHead>,
1113 head_ref: MembershipHeadRef,
1114 },
1115 Serial,
1116}
1117
1118#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1119#[serde(rename_all = "snake_case", deny_unknown_fields)]
1120enum DurableFounderMembershipJournal {
1121 MergeConcurrent {
1122 entry_ref: MembershipEntryRef,
1123 entry_bytes: Vec<u8>,
1124 entry_prepared: PreparedExactObject,
1125 head_ref: MembershipHeadRef,
1126 head_bytes: Vec<u8>,
1127 head_prepared: PreparedExactObject,
1128 },
1129 Serial,
1130}
1131
1132impl DurableFounderMembershipJournal {
1133 fn from_graph(graph: &DurableFounderMembership) -> Self {
1134 match graph {
1135 DurableFounderMembership::MergeConcurrent {
1136 entry,
1137 entry_ref,
1138 head,
1139 head_ref,
1140 } => Self::MergeConcurrent {
1141 entry_ref: entry_ref.clone(),
1142 entry_bytes: entry.bytes.clone(),
1143 entry_prepared: entry.prepared.clone(),
1144 head_ref: head_ref.clone(),
1145 head_bytes: head.bytes.clone(),
1146 head_prepared: head.prepared.clone(),
1147 },
1148 DurableFounderMembership::Serial => Self::Serial,
1149 }
1150 }
1151
1152 fn into_graph(self) -> Result<DurableFounderMembership, DbError> {
1153 match self {
1154 Self::MergeConcurrent {
1155 entry_ref,
1156 entry_bytes,
1157 entry_prepared,
1158 head_ref,
1159 head_bytes,
1160 head_prepared,
1161 } => {
1162 let entry_value: MembershipEntry =
1163 serde_json::from_slice(&entry_bytes).map_err(|error| {
1164 DbError::Message(format!("local founder membership entry: {error}"))
1165 })?;
1166 let head_value: AuthorHead =
1167 serde_json::from_slice(&head_bytes).map_err(|error| {
1168 DbError::Message(format!("local founder membership head: {error}"))
1169 })?;
1170 Ok(DurableFounderMembership::MergeConcurrent {
1171 entry: ExactProtocolObject {
1172 value: entry_value,
1173 bytes: entry_bytes,
1174 object: entry_prepared.reference().clone(),
1175 prepared: entry_prepared,
1176 },
1177 entry_ref,
1178 head: ExactProtocolObject {
1179 value: head_value,
1180 bytes: head_bytes,
1181 object: head_prepared.reference().clone(),
1182 prepared: head_prepared,
1183 },
1184 head_ref,
1185 })
1186 }
1187 Self::Serial => Ok(DurableFounderMembership::Serial),
1188 }
1189 }
1190}
1191
1192#[derive(Debug, Clone, PartialEq, Eq)]
1193pub(crate) enum FounderMembershipRefs {
1194 MergeConcurrent {
1195 entry: MembershipEntryRef,
1196 head: MembershipHeadRef,
1197 },
1198 Serial,
1199}
1200
1201fn founder_graph_identity(graph: &DurableFounderGraph) -> ObjectHash {
1202 let membership = match &graph.membership {
1203 DurableFounderMembership::MergeConcurrent {
1204 entry,
1205 entry_ref,
1206 head,
1207 head_ref,
1208 } => serde_json::to_vec(&(
1209 entry_ref,
1210 &entry.bytes,
1211 &entry.prepared,
1212 head_ref,
1213 &head.bytes,
1214 &head.prepared,
1215 )),
1216 DurableFounderMembership::Serial => serde_json::to_vec(&"serial"),
1217 }
1218 .expect("founder membership graph serialization cannot fail");
1219 ObjectHash::digest(
1220 &serde_json::to_vec(&(
1221 &graph.root.bytes,
1222 &graph.root.prepared,
1223 &graph.registration.bytes,
1224 &graph.registration.prepared,
1225 &graph.initial_ack_ref,
1226 &graph.initial_ack.bytes,
1227 &graph.initial_ack.prepared,
1228 membership,
1229 ))
1230 .expect("founder graph serialization cannot fail"),
1231 )
1232}
1233
1234fn load_store_root_authority_on(
1235 conn: &Connection,
1236) -> Result<Option<(crate::sync::store_commit::StoreRootRef, StoreProtocolRoot)>, DbError> {
1237 conn.query_row(
1238 "SELECT store_root_hash, store_protocol_root_bytes, store_root_object \
1239 FROM store_protocol_root_authority WHERE singleton = 1",
1240 [],
1241 |row| {
1242 Ok((
1243 row.get::<_, String>(0)?,
1244 row.get::<_, Vec<u8>>(1)?,
1245 row.get::<_, String>(2)?,
1246 ))
1247 },
1248 )
1249 .optional()
1250 .map_err(DbError::from)?
1251 .map(|(hash, bytes, object)| {
1252 let value = StoreProtocolRoot::parse(&bytes)
1253 .map_err(|error| DbError::Message(format!("Store root authority bytes: {error}")))?;
1254 let store_root_hash: ObjectHash = hash.parse().map_err(|error| {
1255 DbError::Message(format!("Store root authority semantic hash: {error}"))
1256 })?;
1257 let object: ExactObjectRef = serde_json::from_str(&object)
1258 .map_err(|error| DbError::Message(format!("Store root authority object: {error}")))?;
1259 if value.object_hash() != store_root_hash {
1260 return Err(DbError::Message(
1261 "Store root authority hash differs from its signed bytes".to_string(),
1262 ));
1263 }
1264 Ok((
1265 crate::sync::store_commit::StoreRootRef {
1266 store_root_id: value.descriptor.store_root_id(),
1267 store_root_hash,
1268 object,
1269 },
1270 value,
1271 ))
1272 })
1273 .transpose()
1274}
1275
1276fn required_store_root_authority_on(
1277 conn: &Connection,
1278) -> Result<crate::sync::store_commit::StoreRootRef, DbError> {
1279 load_store_root_authority_on(conn)?
1280 .map(|(reference, _)| reference)
1281 .ok_or_else(|| DbError::Message("exact Store root authority is absent".to_string()))
1282}
1283
1284fn install_store_root_authority_on(
1285 conn: &Connection,
1286 reference: &crate::sync::store_commit::StoreRootRef,
1287 bytes: &[u8],
1288) -> Result<(), DbError> {
1289 let value = StoreProtocolRoot::parse(bytes)
1290 .map_err(|error| DbError::Message(format!("install Store root authority: {error}")))?;
1291 if value.object_hash() != reference.store_root_hash {
1292 return Err(DbError::Message(
1293 "installed Store root reference differs from its signed bytes".to_string(),
1294 ));
1295 }
1296 let object = serde_json::to_string(&reference.object)
1297 .map_err(|error| DbError::Message(format!("serialize Store root authority: {error}")))?;
1298 let existing = load_store_root_authority_on(conn)?;
1299 if let Some((existing_reference, existing_value)) = existing {
1300 if existing_reference == *reference && existing_value == value {
1301 return Ok(());
1302 }
1303 return Err(DbError::Message(
1304 "database already trusts a different exact Store root".to_string(),
1305 ));
1306 }
1307 conn.execute(
1308 "INSERT INTO store_protocol_root_authority \
1309 (singleton, store_root_hash, store_protocol_root_bytes, store_root_object) \
1310 VALUES (1, ?1, ?2, ?3)",
1311 rusqlite::params![reference.store_root_hash.to_string(), bytes, object],
1312 )
1313 .map(|_| ())
1314 .map_err(DbError::from)
1315}
1316
1317fn install_store_founder_state_on(
1318 conn: &Connection,
1319 root: &crate::sync::store_commit::StoreRootRef,
1320 founder_reference: &StoreDeviceRegistrationRef,
1321 founder: &StoreDeviceRegistration,
1322 founder_bytes: &[u8],
1323 genesis: &ResolvedStoreDeviceState,
1324) -> Result<(), DbError> {
1325 if founder.store_root != *root {
1326 return Err(DbError::Message(
1327 "Store founder registration belongs to another exact root".to_string(),
1328 ));
1329 }
1330 founder_reference
1331 .verify_registration(founder)
1332 .map_err(|error| DbError::Message(error.to_string()))?;
1333 if founder.to_bytes() != founder_bytes {
1334 return Err(DbError::Message(
1335 "Store founder registration differs from its exact bytes".to_string(),
1336 ));
1337 }
1338 let founder_authority = crate::sync::store_commit::StoreDeviceRegistrationActivation::Founder {
1339 root: root.clone(),
1340 };
1341 let founder_values = (
1342 founder_reference.registration_hash.to_string(),
1343 founder.author_pubkey.clone(),
1344 founder.device_signing_pubkey.clone(),
1345 founder_bytes.to_vec(),
1346 serde_json::to_string(founder_reference).map_err(|error| {
1347 DbError::Message(format!("serialize Store founder registration ref: {error}"))
1348 })?,
1349 serde_json::to_string(&founder_authority).map_err(|error| {
1350 DbError::Message(format!("serialize Store founder activation: {error}"))
1351 })?,
1352 );
1353 conn.execute(
1354 "INSERT INTO store_device_registration_activations
1355 (device_id, registration_hash, author_pubkey, device_signing_pubkey,
1356 registration_bytes, registration_object, activation_authority)
1357 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
1358 ON CONFLICT(device_id) DO NOTHING",
1359 rusqlite::params![
1360 founder.device_id.to_string(),
1361 &founder_values.0,
1362 &founder_values.1,
1363 &founder_values.2,
1364 &founder_values.3,
1365 &founder_values.4,
1366 &founder_values.5,
1367 ],
1368 )
1369 .map_err(DbError::from)?;
1370 let stored_founder: (String, String, String, Vec<u8>, String, String) = conn
1371 .query_row(
1372 "SELECT registration_hash, author_pubkey, device_signing_pubkey,
1373 registration_bytes, registration_object, activation_authority
1374 FROM store_device_registration_activations WHERE device_id = ?1",
1375 [founder.device_id.to_string()],
1376 |row| {
1377 Ok((
1378 row.get(0)?,
1379 row.get(1)?,
1380 row.get(2)?,
1381 row.get(3)?,
1382 row.get(4)?,
1383 row.get(5)?,
1384 ))
1385 },
1386 )
1387 .map_err(DbError::from)?;
1388 if stored_founder != founder_values {
1389 return Err(DbError::Message(
1390 "Store founder activation differs from installed exact authority".to_string(),
1391 ));
1392 }
1393 let genesis = serde_json::to_string(genesis)
1394 .map_err(|error| DbError::Message(format!("serialize Store device genesis: {error}")))?;
1395 conn.execute(
1396 "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
1397 (STORE_DEVICE_GENESIS_STATE_KEY, &genesis),
1398 )
1399 .map_err(DbError::from)?;
1400 let stored_genesis: String = conn
1401 .query_row(
1402 "SELECT value FROM protocol_state WHERE key = ?1",
1403 [STORE_DEVICE_GENESIS_STATE_KEY],
1404 |row| row.get(0),
1405 )
1406 .map_err(DbError::from)?;
1407 if stored_genesis != genesis {
1408 return Err(DbError::Message(
1409 "Store device genesis differs from installed exact authority".to_string(),
1410 ));
1411 }
1412 Ok(())
1413}
1414
1415fn validate_founder_graph(graph: &DurableFounderGraph) -> Result<(), DbError> {
1416 let root = StoreProtocolRoot::parse(&graph.root.bytes)
1417 .map_err(|error| DbError::Message(format!("founder Store root: {error}")))?;
1418 if root != graph.root.value
1419 || root.object_hash() != graph.root.value.object_hash()
1420 || graph.root.object != *graph.root.prepared.reference()
1421 {
1422 return Err(DbError::Message(
1423 "founder Store root differs from its prepared exact object".to_string(),
1424 ));
1425 }
1426 let root_ref = crate::sync::store_commit::StoreRootRef {
1427 store_root_id: root.descriptor.store_root_id(),
1428 store_root_hash: root.object_hash(),
1429 object: graph.root.object.clone(),
1430 };
1431 let registration = StoreDeviceRegistration::parse_at(
1432 &graph.registration.bytes,
1433 &root_ref,
1434 graph.registration.value.device_id,
1435 )
1436 .map_err(|error| DbError::Message(format!("founder Store registration: {error}")))?;
1437 if registration != graph.registration.value
1438 || graph.registration.object != *graph.registration.prepared.reference()
1439 || registration.author_pubkey != root.descriptor.founder_pubkey
1440 || graph.registration.object.slot() != &root.descriptor.founder_registration
1441 || registration.provider != root.descriptor.founder_provider_admin.provider
1442 || !matches!(
1443 registration.origin,
1444 crate::sync::store_commit::StoreDeviceRegistrationOrigin::Founder { .. }
1445 )
1446 {
1447 return Err(DbError::Message(
1448 "founder registration differs from its root or prepared exact object".to_string(),
1449 ));
1450 }
1451 let registration_ref = StoreDeviceRegistrationRef::from_registration(
1452 ®istration,
1453 graph.registration.object.clone(),
1454 );
1455 let initial_ack = StoreAck::parse_at(
1456 &graph.initial_ack.bytes,
1457 root_ref.store_root_hash,
1458 &graph.initial_ack_ref,
1459 ®istration,
1460 )
1461 .map_err(|error| DbError::Message(format!("founder initial acknowledgement: {error}")))?;
1462 if initial_ack != graph.initial_ack.value
1463 || graph.initial_ack_ref.revision != 1
1464 || graph.initial_ack_ref.object != graph.initial_ack.object
1465 || graph.initial_ack.object != *graph.initial_ack.prepared.reference()
1466 || initial_ack.predecessor.is_some()
1467 || initial_ack.author_registration != registration_ref
1468 || match (&root.descriptor.write_policy, &initial_ack.store_cut) {
1469 (
1470 crate::WritePolicy::MergeConcurrent,
1471 crate::sync::store_commit::StoreHistoryCut::MergeConcurrent(commits),
1472 ) => !commits.is_empty(),
1473 (
1474 crate::WritePolicy::Serial,
1475 crate::sync::store_commit::StoreHistoryCut::Serial(
1476 crate::sync::store_commit::StoreSerialPredecessor::Genesis {
1477 root: ack_root,
1478 founder_registration,
1479 },
1480 ),
1481 ) => ack_root != &root_ref || founder_registration != ®istration_ref,
1482 _ => true,
1483 }
1484 {
1485 return Err(DbError::Message(
1486 "founder initial acknowledgement differs from its exact root graph".to_string(),
1487 ));
1488 }
1489 match (&root.descriptor.membership, &graph.membership) {
1490 (
1491 crate::sync::store_commit::StoreMembershipGenesis::MergeConcurrent { .. },
1492 DurableFounderMembership::MergeConcurrent {
1493 entry,
1494 entry_ref,
1495 head,
1496 head_ref,
1497 },
1498 ) => {
1499 let parsed_entry: MembershipEntry = serde_json::from_slice(&entry.bytes)
1500 .map_err(|error| DbError::Message(format!("founder membership entry: {error}")))?;
1501 if parsed_entry != entry.value
1502 || root
1503 .descriptor
1504 .validate_merge_founder_entry(&parsed_entry)
1505 .is_err()
1506 || entry_ref.coord != parsed_entry.coord()
1507 || entry_ref.object != entry.object
1508 || entry.object != *entry.prepared.reference()
1509 {
1510 return Err(DbError::Message(
1511 "founder membership entry differs from its root or exact reference".to_string(),
1512 ));
1513 }
1514 let parsed_head: AuthorHead = serde_json::from_slice(&head.bytes)
1515 .map_err(|error| DbError::Message(format!("founder membership head: {error}")))?;
1516 let anchor = parsed_entry.change.membership_anchor().ok_or_else(|| {
1517 DbError::Message("founder entry has no Store membership anchor".to_string())
1518 })?;
1519 let crate::sync::store_commit::GrantStreamAnchor::StoreMembership { first_slot } =
1520 anchor
1521 else {
1522 return Err(DbError::Message(
1523 "founder membership entry uses a recovery anchor".to_string(),
1524 ));
1525 };
1526 if parsed_head != head.value
1527 || !parsed_head.verify(®istration)
1528 || parsed_head.author_registration != registration_ref
1529 || parsed_head.entry != *entry_ref
1530 || parsed_head.predecessor.is_some()
1531 || parsed_head.entry_coord() != parsed_entry.coord()
1532 || head_ref.coord != parsed_entry.coord()
1533 || head_ref.head_hash != parsed_head.head_hash()
1534 || head_ref.object != head.object
1535 || head.object != *head.prepared.reference()
1536 || head.object.slot() != &first_slot
1537 || parsed_head.successor.activation
1538 != StreamActivationId::store_membership(
1539 &root_ref,
1540 ®istration_ref,
1541 &parsed_entry.author_owner_grant,
1542 &crate::sync::store_commit::GrantStreamAnchor::StoreMembership {
1543 first_slot: first_slot.clone(),
1544 },
1545 )
1546 {
1547 return Err(DbError::Message(
1548 "founder membership head differs from its exact root graph".to_string(),
1549 ));
1550 }
1551 }
1552 (
1553 crate::sync::store_commit::StoreMembershipGenesis::Serial,
1554 DurableFounderMembership::Serial,
1555 ) => {}
1556 _ => {
1557 return Err(DbError::Message(
1558 "founder membership graph differs from the root policy".to_string(),
1559 ))
1560 }
1561 }
1562 Ok(())
1563}
1564
1565fn consume_store_creation_probes_on(
1566 conn: &Connection,
1567 graph: &DurableFounderGraph,
1568) -> Result<(), DbError> {
1569 use crate::sync::provider::{
1570 ExactProbeProgress, ProviderProbeJournalRecord, SerialProbeProgress,
1571 };
1572 use crate::sync::store_protocol_root::{
1573 StoreCreationAttempt, StoreCreationProbeIds, STORE_CREATION_ATTEMPT_STATE_KEY,
1574 };
1575
1576 let attempt_json: String = conn
1577 .query_row(
1578 "SELECT value FROM protocol_state WHERE key = ?1",
1579 [STORE_CREATION_ATTEMPT_STATE_KEY],
1580 |row| row.get(0),
1581 )
1582 .map_err(DbError::from)?;
1583 let attempt: StoreCreationAttempt = serde_json::from_str(&attempt_json)
1584 .map_err(|error| DbError::Message(format!("parse Store creation attempt: {error}")))?;
1585 let StoreCreationAttempt::FounderGraphReserved(graph_reservation) = attempt else {
1586 return Err(DbError::Message(
1587 "Store creation attempt has not reserved the complete founder graph".to_string(),
1588 ));
1589 };
1590 let reservation = &graph_reservation.descriptor;
1591 let descriptor = &graph.root.value.descriptor;
1592 let founder = reservation.membership.founder();
1593 let authority = &founder.root.authority;
1594 if authority.creation_id != descriptor.creation_id
1595 || authority.founder_grant != descriptor.founder_grant
1596 || authority.provider_admin_grant != descriptor.founder_provider_admin.grant_id
1597 || authority.binding.store != descriptor.provider
1598 || authority.binding.device != descriptor.founder_provider_admin.provider
1599 || authority.founder_pubkey != descriptor.founder_pubkey
1600 || authority.write_policy != descriptor.write_policy
1601 || authority.schema_version != descriptor.schema_version
1602 || authority.sync_routing_hash != descriptor.sync_routing_hash
1603 || founder.root.root_slot != descriptor.root_slot
1604 || founder.registration_slot != descriptor.founder_registration
1605 || &reservation.recovery_slot != descriptor.founder_recovery.first_slot()
1606 || match (&reservation.membership, &descriptor.membership) {
1607 (
1608 crate::sync::store_protocol_root::MembershipReservation::MergeConcurrent {
1609 first_slot,
1610 ..
1611 },
1612 crate::sync::store_commit::StoreMembershipGenesis::MergeConcurrent {
1613 founder_membership,
1614 },
1615 ) => founder_membership.first_slot() != first_slot,
1616 (
1617 crate::sync::store_protocol_root::MembershipReservation::Serial { .. },
1618 crate::sync::store_commit::StoreMembershipGenesis::Serial,
1619 ) => false,
1620 _ => true,
1621 }
1622 {
1623 return Err(DbError::Message(
1624 "signed Store descriptor differs from its durable creation attempt".to_string(),
1625 ));
1626 }
1627 if graph.registration.value.store_commits != graph_reservation.store_commits
1628 || graph.registration.value.acknowledgements != graph_reservation.acknowledgements
1629 || graph.registration.value.snapshots != graph_reservation.snapshots
1630 || graph.initial_ack.value.last_sync != authority.founder_timestamp
1631 || graph.initial_ack.value.successor.next_slot != graph_reservation.next_ack_slot
1632 || match (&graph.membership, &graph_reservation.membership) {
1633 (
1634 DurableFounderMembership::MergeConcurrent { entry, head, .. },
1635 crate::sync::store_protocol_root::FounderMembershipPublicationReservation::MergeConcurrent {
1636 next_head_slot,
1637 },
1638 ) => {
1639 entry.value.created_at != authority.founder_timestamp
1640 || head.value.successor.next_slot != *next_head_slot
1641 }
1642 (
1643 DurableFounderMembership::Serial,
1644 crate::sync::store_protocol_root::FounderMembershipPublicationReservation::Serial,
1645 ) => false,
1646 _ => true,
1647 }
1648 {
1649 return Err(DbError::Message(
1650 "signed founder graph differs from its durable slot reservation".to_string(),
1651 ));
1652 }
1653
1654 let load_probe = |probe_id: crate::sync::provider::ProviderProbeId| {
1655 let key = format!("provider_probe/{}", hex::encode(probe_id.as_bytes()));
1656 let value: String = conn
1657 .query_row(
1658 "SELECT value FROM protocol_state WHERE key = ?1",
1659 [&key],
1660 |row| row.get(0),
1661 )
1662 .map_err(DbError::from)?;
1663 let record = serde_json::from_str(&value)
1664 .map_err(|error| DbError::Message(format!("parse provider probe journal: {error}")))?;
1665 Ok::<_, DbError>((key, record))
1666 };
1667 let (exact_id, serial_id) = match authority.probes {
1668 StoreCreationProbeIds::MergeConcurrent { exact_slots } => (exact_slots, None),
1669 StoreCreationProbeIds::Serial {
1670 exact_slots,
1671 serial_coordination,
1672 } => (exact_slots, Some(serial_coordination)),
1673 };
1674 let (exact_key, exact) = load_probe(exact_id)?;
1675 let ProviderProbeJournalRecord::Exact(exact) = exact else {
1676 return Err(DbError::Message(
1677 "Store creation exact probe id names another probe kind".to_string(),
1678 ));
1679 };
1680 let ExactProbeProgress::ReceiptReady { receipt } = exact.progress else {
1681 return Err(DbError::Message(
1682 "Store creation exact probe has no terminal receipt".to_string(),
1683 ));
1684 };
1685 if receipt != descriptor.founder_provider_admin.capability.exact_slots {
1686 return Err(DbError::Message(
1687 "signed Store descriptor differs from its terminal exact probe".to_string(),
1688 ));
1689 }
1690 let mut consumed_keys = vec![exact_key];
1691 match (
1692 serial_id,
1693 &descriptor
1694 .founder_provider_admin
1695 .capability
1696 .serial_coordination,
1697 ) {
1698 (None, None) => {}
1699 (Some(probe_id), Some(expected)) => {
1700 let (key, serial) = load_probe(probe_id)?;
1701 let ProviderProbeJournalRecord::Serial(serial) = serial else {
1702 return Err(DbError::Message(
1703 "Store creation serial probe id names another probe kind".to_string(),
1704 ));
1705 };
1706 let SerialProbeProgress::ReceiptReady { receipt } = serial.progress else {
1707 return Err(DbError::Message(
1708 "Store creation serial probe has no terminal receipt".to_string(),
1709 ));
1710 };
1711 if &receipt != expected {
1712 return Err(DbError::Message(
1713 "signed Store descriptor differs from its terminal serial probe".to_string(),
1714 ));
1715 }
1716 consumed_keys.push(key);
1717 }
1718 _ => {
1719 return Err(DbError::Message(
1720 "Store creation probe policy differs from the signed descriptor".to_string(),
1721 ))
1722 }
1723 }
1724 consumed_keys.push(STORE_CREATION_ATTEMPT_STATE_KEY.to_string());
1725 for key in consumed_keys {
1726 let deleted = conn
1727 .execute("DELETE FROM protocol_state WHERE key = ?1", [key])
1728 .map_err(DbError::from)?;
1729 if deleted != 1 {
1730 return Err(DbError::Message(
1731 "Store creation journal disappeared during typed consumption".to_string(),
1732 ));
1733 }
1734 }
1735 Ok(())
1736}
1737
1738fn load_local_store_founder_graph_on(
1739 conn: &Connection,
1740) -> Result<Option<DurableFounderGraph>, DbError> {
1741 let owned_rows: i64 = conn
1742 .query_row(
1743 "SELECT EXISTS(SELECT 1 FROM local_store_protocol_root) \
1744 + EXISTS(SELECT 1 FROM local_store_device_registration) \
1745 + EXISTS(SELECT 1 FROM local_store_founder_graph)",
1746 [],
1747 |row| row.get(0),
1748 )
1749 .map_err(DbError::from)?;
1750 if owned_rows == 0 {
1751 return Ok(None);
1752 }
1753 if owned_rows != 3 {
1754 return Err(DbError::Message(
1755 "local Store founder graph is only partially durable".to_string(),
1756 ));
1757 }
1758 let raw = conn
1759 .query_row(
1760 "SELECT r.store_root_hash, r.store_protocol_root_bytes, r.prepared_object, \
1761 d.device_id, d.registration_hash, d.registration_bytes, d.prepared_object, \
1762 d.initial_ack_ref, d.initial_ack_bytes, d.initial_ack_prepared, d.state, \
1763 g.membership_graph \
1764 FROM local_store_protocol_root r \
1765 CROSS JOIN local_store_device_registration d \
1766 CROSS JOIN local_store_founder_graph g \
1767 WHERE r.singleton = 1 AND d.singleton = 1 AND g.singleton = 1",
1768 [],
1769 |row| {
1770 Ok((
1771 row.get::<_, String>(0)?,
1772 row.get::<_, Vec<u8>>(1)?,
1773 row.get::<_, String>(2)?,
1774 row.get::<_, String>(3)?,
1775 row.get::<_, String>(4)?,
1776 row.get::<_, Vec<u8>>(5)?,
1777 row.get::<_, String>(6)?,
1778 row.get::<_, String>(7)?,
1779 row.get::<_, Vec<u8>>(8)?,
1780 row.get::<_, String>(9)?,
1781 row.get::<_, String>(10)?,
1782 row.get::<_, String>(11)?,
1783 ))
1784 },
1785 )
1786 .map_err(DbError::from)?;
1787 let (
1788 root_hash,
1789 root_bytes,
1790 root_prepared,
1791 device_id,
1792 registration_hash,
1793 registration_bytes,
1794 registration_prepared,
1795 initial_ack_ref,
1796 initial_ack_bytes,
1797 initial_ack_prepared,
1798 registration_state,
1799 membership_graph,
1800 ) = raw;
1801 let registration_state: LocalDeviceRegistrationState =
1802 serde_json::from_str(®istration_state).map_err(|error| {
1803 DbError::Message(format!("local registration journal state: {error}"))
1804 })?;
1805 let root_value = StoreProtocolRoot::parse(&root_bytes)
1806 .map_err(|error| DbError::Message(format!("local founder Store root: {error}")))?;
1807 let root_prepared: PreparedExactObject = serde_json::from_str(&root_prepared)
1808 .map_err(|error| DbError::Message(format!("local founder Store root object: {error}")))?;
1809 let store_root_hash: ObjectHash = root_hash
1810 .parse()
1811 .map_err(|error| DbError::Message(format!("local founder Store root hash: {error}")))?;
1812 if store_root_hash != root_value.object_hash() {
1813 return Err(DbError::Message(
1814 "local founder Store root hash differs from its bytes".to_string(),
1815 ));
1816 }
1817 let root_ref = crate::sync::store_commit::StoreRootRef {
1818 store_root_id: root_value.descriptor.store_root_id(),
1819 store_root_hash,
1820 object: root_prepared.reference().clone(),
1821 };
1822 let parsed_device_id = device_id
1823 .parse()
1824 .map_err(|error| DbError::Message(format!("local founder device id: {error}")))?;
1825 let registration_value =
1826 StoreDeviceRegistration::parse_at(®istration_bytes, &root_ref, parsed_device_id)
1827 .map_err(|error| {
1828 DbError::Message(format!("local founder Store registration: {error}"))
1829 })?;
1830 let parsed_registration_hash: ObjectHash = registration_hash.parse().map_err(|error| {
1831 DbError::Message(format!("local founder Store registration hash: {error}"))
1832 })?;
1833 if parsed_registration_hash != registration_value.registration_hash() {
1834 return Err(DbError::Message(
1835 "local founder registration hash differs from its bytes".to_string(),
1836 ));
1837 }
1838 let registration_prepared: PreparedExactObject = serde_json::from_str(®istration_prepared)
1839 .map_err(|error| {
1840 DbError::Message(format!("local founder registration object: {error}"))
1841 })?;
1842 let initial_ack_ref: StoreAckRef = serde_json::from_str(&initial_ack_ref)
1843 .map_err(|error| DbError::Message(format!("local founder initial ack ref: {error}")))?;
1844 let initial_ack_value = StoreAck::parse_at(
1845 &initial_ack_bytes,
1846 root_ref.store_root_hash,
1847 &initial_ack_ref,
1848 ®istration_value,
1849 )
1850 .map_err(|error| DbError::Message(format!("local founder initial ack: {error}")))?;
1851 let initial_ack_prepared: PreparedExactObject = serde_json::from_str(&initial_ack_prepared)
1852 .map_err(|error| DbError::Message(format!("local founder initial ack object: {error}")))?;
1853 let membership = serde_json::from_str::<DurableFounderMembershipJournal>(&membership_graph)
1854 .map_err(|error| DbError::Message(format!("local founder membership graph: {error}")))?
1855 .into_graph()?;
1856 let graph = DurableFounderGraph {
1857 root: ExactProtocolObject {
1858 value: root_value,
1859 bytes: root_bytes,
1860 object: root_prepared.reference().clone(),
1861 prepared: root_prepared,
1862 },
1863 registration: ExactProtocolObject {
1864 value: registration_value,
1865 bytes: registration_bytes,
1866 object: registration_prepared.reference().clone(),
1867 prepared: registration_prepared,
1868 },
1869 initial_ack: ExactProtocolObject {
1870 value: initial_ack_value,
1871 bytes: initial_ack_bytes,
1872 object: initial_ack_prepared.reference().clone(),
1873 prepared: initial_ack_prepared,
1874 },
1875 initial_ack_ref,
1876 membership,
1877 registration_state,
1878 };
1879 validate_founder_graph(&graph)?;
1880 Ok(Some(graph))
1881}
1882
1883#[derive(Debug, Clone, PartialEq, Eq)]
1885pub(crate) struct BlobActivation {
1886 pub coord: StoreCommitCoord,
1887}
1888
1889#[derive(Debug, Clone, PartialEq, Eq)]
1890pub(crate) struct PreparedAudiencePackage {
1891 remote_object_id: ObjectHash,
1892 package: AudiencePackage,
1893 semantic_bytes: Vec<u8>,
1894 stored_bytes: Vec<u8>,
1895 object: ExactObjectRef,
1896}
1897
1898impl PreparedAudiencePackage {
1899 fn from_remote(remote: RemoteObjectRecord) -> Result<Self, DbError> {
1900 remote
1901 .validate()
1902 .map_err(|error| DbError::Message(format!("prepared remote package: {error}")))?;
1903 let is_package = match &remote {
1904 RemoteObjectRecord::CandidateCommit(_) | RemoteObjectRecord::RetainedAuthority(_) => {
1905 false
1906 }
1907 RemoteObjectRecord::CandidateExclusive(record) => matches!(
1908 record.identity.domain,
1909 CandidateExclusiveObjectDomain::StorePackage
1910 | CandidateExclusiveObjectDomain::CirclePackage { .. }
1911 ),
1912 RemoteObjectRecord::SharedLiveSet(record) => matches!(
1913 record.identity.domain,
1914 SharedLiveSetObjectDomain::StorePackage | SharedLiveSetObjectDomain::CirclePackage
1915 ),
1916 };
1917 if !is_package {
1918 return Err(DbError::Message(
1919 "prepared package index references a non-package remote object".to_string(),
1920 ));
1921 }
1922 let remote_object_id = remote.object_id();
1923 let semantic_bytes = remote.bytes().canonical_semantic_bytes().to_vec();
1924 let object = remote.object().clone();
1925 let stored_bytes = remote
1926 .bytes()
1927 .stored()
1928 .inline_bytes()
1929 .ok_or_else(|| {
1930 DbError::Message("prepared package remote object is not inline".to_string())
1931 })?
1932 .to_vec();
1933 Self::new(remote_object_id, semantic_bytes, stored_bytes, object)
1934 }
1935
1936 pub(crate) fn new(
1937 remote_object_id: ObjectHash,
1938 semantic_bytes: Vec<u8>,
1939 stored_bytes: Vec<u8>,
1940 object: ExactObjectRef,
1941 ) -> Result<Self, DbError> {
1942 let package = AudiencePackage::parse(&semantic_bytes)
1943 .map_err(|error| DbError::Message(format!("prepared audience package: {error}")))?;
1944 object.verify(&stored_bytes).map_err(|error| {
1945 DbError::Message(format!("prepared audience package stored bytes: {error}"))
1946 })?;
1947 Ok(Self {
1948 remote_object_id,
1949 package,
1950 semantic_bytes,
1951 stored_bytes,
1952 object,
1953 })
1954 }
1955
1956 pub(crate) fn remote_object_id(&self) -> ObjectHash {
1957 self.remote_object_id
1958 }
1959
1960 pub(crate) fn package(&self) -> &AudiencePackage {
1961 &self.package
1962 }
1963
1964 pub(crate) fn semantic_bytes(&self) -> &[u8] {
1965 &self.semantic_bytes
1966 }
1967
1968 pub(crate) fn stored_bytes(&self) -> &[u8] {
1969 &self.stored_bytes
1970 }
1971
1972 pub(crate) fn object(&self) -> &ExactObjectRef {
1973 &self.object
1974 }
1975}
1976
1977#[derive(Debug, Clone, PartialEq, Eq)]
1978pub(crate) struct PreparedAudienceBlob {
1979 remote_object_id: ObjectHash,
1980 audience: RemoteAudience,
1981 blob: StoredBlobRef,
1982 spool_path: Option<PathBuf>,
1983}
1984
1985impl PreparedAudienceBlob {
1986 pub(crate) fn from_remote(
1987 audience: RemoteAudience,
1988 expected_locator_hash: &str,
1989 remote: RemoteObjectRecord,
1990 spool_path: Option<PathBuf>,
1991 ) -> Result<Self, DbError> {
1992 remote
1993 .validate()
1994 .map_err(|error| DbError::Message(format!("prepared remote blob: {error}")))?;
1995 if !matches!(
1996 &remote,
1997 RemoteObjectRecord::SharedLiveSet(record)
1998 if record.identity.domain == SharedLiveSetObjectDomain::StoredBlob
1999 ) {
2000 return Err(DbError::Message(
2001 "prepared blob index references a non-blob remote object".to_string(),
2002 ));
2003 }
2004 let locator = BlobLocator::parse(remote.bytes().canonical_semantic_bytes())
2005 .map_err(|error| DbError::Message(format!("prepared blob locator: {error}")))?;
2006 if locator.locator_hash().to_string() != expected_locator_hash {
2007 return Err(DbError::Message(format!(
2008 "prepared blob locator hashes to {}, indexed as {expected_locator_hash}",
2009 locator.locator_hash()
2010 )));
2011 }
2012 if locator.audience() != audience {
2013 return Err(DbError::Message(format!(
2014 "prepared blob index audience {audience:?} differs from locator audience {:?}",
2015 locator.audience()
2016 )));
2017 }
2018 let requires_upload = matches!(
2019 &remote,
2020 RemoteObjectRecord::SharedLiveSet(record)
2021 if matches!(record.state, crate::sync::remote_object::OwnedObjectState::Prepared { .. })
2022 );
2023 if requires_upload && spool_path.is_none() {
2024 return Err(DbError::Message(
2025 "prepared blob awaiting upload has no local spool".to_string(),
2026 ));
2027 }
2028 if spool_path.as_ref().is_some_and(|path| !path.is_absolute()) {
2029 return Err(DbError::Message(
2030 "prepared blob local spool path is not absolute".to_string(),
2031 ));
2032 }
2033 let blob = StoredBlobRef::new(locator, remote.object().clone())
2034 .map_err(|error| DbError::Message(format!("prepared blob reference: {error}")))?;
2035 Ok(Self {
2036 remote_object_id: remote.object_id(),
2037 audience,
2038 blob,
2039 spool_path,
2040 })
2041 }
2042
2043 pub(crate) fn remote_object_id(&self) -> ObjectHash {
2044 self.remote_object_id
2045 }
2046
2047 pub(crate) fn audience(&self) -> &RemoteAudience {
2048 &self.audience
2049 }
2050
2051 pub(crate) fn blob(&self) -> &StoredBlobRef {
2052 &self.blob
2053 }
2054
2055 pub(crate) fn spool_path(&self) -> Option<&Path> {
2056 self.spool_path.as_deref()
2057 }
2058}
2059
2060#[derive(Debug, Clone)]
2061pub(crate) struct PreparedAudienceObjects {
2062 pub packages: Vec<PreparedAudiencePackage>,
2063 pub blobs: Vec<PreparedAudienceBlob>,
2064}
2065
2066pub(crate) struct PreparedRemoteObject {
2067 pub record: RemoteObjectRecord,
2068 pub spool_path: Option<PathBuf>,
2069}
2070
2071#[derive(Debug, Clone, PartialEq, Eq)]
2072pub(crate) enum MakeRemoteIntentState {
2073 Uploading,
2074 Cancelling,
2075 Publishing(WriteId),
2076}
2077
2078#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2079pub(crate) enum StoredBlobReferenceState {
2080 NotLiveRemote,
2081 LiveRemote,
2082 Unresolved,
2083}
2084
2085fn validate_prepared_audience_blob_graph(
2086 object_ids: &std::collections::BTreeSet<ObjectHash>,
2087 audiences: &PreparedAudienceObjects,
2088) -> Result<(), DbError> {
2089 let mut indexed = std::collections::BTreeSet::new();
2090 for package in &audiences.packages {
2091 if !indexed.insert(package.remote_object_id()) {
2092 return Err(DbError::Message(
2093 "prepared audience objects contain a duplicate package body".to_string(),
2094 ));
2095 }
2096 }
2097 for blob in &audiences.blobs {
2098 if !indexed.insert(blob.remote_object_id()) {
2099 return Err(DbError::Message(
2100 "prepared audience objects contain a duplicate blob body".to_string(),
2101 ));
2102 }
2103 }
2104 if &indexed != object_ids {
2105 return Err(DbError::Message(
2106 "closed remote objects differ from package/blob indexes".to_string(),
2107 ));
2108 }
2109 validate_prepared_audience_blob_bindings(audiences)
2110}
2111
2112fn validate_prepared_audience_blob_bindings(
2113 audiences: &PreparedAudienceObjects,
2114) -> Result<(), DbError> {
2115 for package in &audiences.packages {
2116 let audience = package.package().audience().remote_audience();
2117 for binding in package.package().blob_bindings() {
2118 if !audiences
2119 .blobs
2120 .iter()
2121 .any(|blob| blob.audience() == &audience && blob.blob() == binding.blob())
2122 {
2123 return Err(DbError::Message(
2124 "prepared package blob binding has no exact blob index".to_string(),
2125 ));
2126 }
2127 }
2128 }
2129 for blob in &audiences.blobs {
2130 if !audiences.packages.iter().any(|package| {
2131 package.package().audience().remote_audience() == *blob.audience()
2132 && package
2133 .package()
2134 .blob_bindings()
2135 .iter()
2136 .any(|binding| binding.blob() == blob.blob())
2137 }) {
2138 return Err(DbError::Message(
2139 "prepared blob index has no exact package binding".to_string(),
2140 ));
2141 }
2142 }
2143 Ok(())
2144}
2145
2146#[cfg(feature = "invariant-tests")]
2147#[doc(hidden)]
2148pub fn exercise_exact_outbound_blob_graph(
2149 circle: bool,
2150 include_body: bool,
2151 include_locator: bool,
2152 include_binding: bool,
2153) -> Result<(), String> {
2154 use crate::blob::BlobScope;
2155 use crate::storage::cloud::ObjectSlot;
2156 use crate::sync::audience_package::RowBlobLocatorBinding;
2157 use crate::sync::circle::CircleId;
2158 use crate::sync::circle_control::CircleControlCoord;
2159 use crate::sync::store_commit::{CandidateFamilyId, StoreCommitCoord};
2160
2161 let store_root_hash = ObjectHash::digest(b"outbound-graph-store");
2162 let write_id = WriteId::from_generated("outbound-graph-write".to_string());
2163 let coord = StoreCommitCoord::Serial { sequence: 1 };
2164 let candidate_family = CandidateFamilyId::from_hash(ObjectHash::digest(b"outbound-family"));
2165 let remote_audience = if circle {
2166 RemoteAudience::Circle(CircleId::from_bytes([7; 16]))
2167 } else {
2168 RemoteAudience::Store
2169 };
2170 let uploader_bytes = b"outbound graph uploader registration";
2171 let uploader = StoreDeviceRegistrationRef {
2172 device_id: "01"
2173 .repeat(32)
2174 .parse::<crate::sync::store_commit::StoreDeviceId>()
2175 .map_err(|error| error.to_string())?,
2176 registration_hash: ObjectHash::digest(uploader_bytes),
2177 object: ExactObjectRef::new(
2178 ObjectSlot::logical("store-v1/registrations/outbound-graph.json".to_string())
2179 .map_err(|error| error.to_string())?,
2180 uploader_bytes.len() as u64,
2181 ObjectHash::digest(uploader_bytes),
2182 ),
2183 };
2184 let locator = BlobLocator::opaque(
2185 "media".to_string(),
2186 "blob-a".to_string(),
2187 uploader,
2188 remote_audience.clone(),
2189 BlobScope::Master,
2190 crate::KeyFingerprint::from_bytes([3; 8]),
2191 7,
2192 ObjectHash::digest(b"content"),
2193 )
2194 .map_err(|error| error.to_string())?;
2195 let stored_bytes = b"sealed-content".to_vec();
2196 let object = ExactObjectRef::new(
2197 ObjectSlot::logical(locator.semantic_key()).map_err(|error| error.to_string())?,
2198 stored_bytes.len() as u64,
2199 ObjectHash::digest(&stored_bytes),
2200 );
2201 let stored = StoredBlobRef::new(locator, object).map_err(|error| error.to_string())?;
2202 let bindings = if include_binding {
2203 vec![
2204 RowBlobLocatorBinding::new("items", "row-a", "stamp-a", "media_blob", stored.clone())
2205 .map_err(|error| error.to_string())?,
2206 ]
2207 } else {
2208 Vec::new()
2209 };
2210 let package = if let RemoteAudience::Circle(circle_id) = remote_audience {
2211 AudiencePackage::circle(
2212 store_root_hash,
2213 candidate_family,
2214 write_id.clone(),
2215 coord,
2216 1,
2217 circle_id,
2218 CircleControlCoord::Serial {
2219 author_pubkey: "author-a".to_string(),
2220 generation: 1,
2221 control_hash: ObjectHash::digest(b"circle-control"),
2222 },
2223 crate::KeyFingerprint::from_bytes([3; 8]),
2224 b"changeset".to_vec(),
2225 bindings,
2226 )
2227 } else {
2228 AudiencePackage::store(
2229 store_root_hash,
2230 candidate_family,
2231 write_id,
2232 coord,
2233 1,
2234 b"changeset".to_vec(),
2235 bindings,
2236 )
2237 }
2238 .map_err(|error| error.to_string())?;
2239 let package_bytes = package.to_bytes();
2240 let package_object = ExactObjectRef::new(
2241 ObjectSlot::logical("test/package".to_string()).map_err(|error| error.to_string())?,
2242 package_bytes.len() as u64,
2243 ObjectHash::digest(&package_bytes),
2244 );
2245 let package_id = ObjectHash::digest(b"package-record");
2246 let blob_id = ObjectHash::digest(b"blob-record");
2247 let packages = vec![PreparedAudiencePackage::new(
2248 package_id,
2249 package_bytes.clone(),
2250 package_bytes,
2251 package_object,
2252 )
2253 .map_err(|error| error.to_string())?];
2254 let blobs = if include_locator {
2255 vec![PreparedAudienceBlob {
2256 remote_object_id: blob_id,
2257 audience: remote_audience,
2258 blob: stored,
2259 spool_path: Some(PathBuf::from("/outbound-blob.spool")),
2260 }]
2261 } else {
2262 Vec::new()
2263 };
2264 let mut object_ids = std::collections::BTreeSet::from([package_id]);
2265 if include_body {
2266 object_ids.insert(blob_id);
2267 }
2268 validate_prepared_audience_blob_graph(&object_ids, &PreparedAudienceObjects { packages, blobs })
2269 .map_err(|error| error.to_string())
2270}
2271
2272#[derive(Debug, Clone)]
2273pub(crate) struct DurableDeviceRegistration {
2274 pub device_id: crate::sync::store_commit::StoreDeviceId,
2275 pub registration_hash: ObjectHash,
2276 pub registration_bytes: Vec<u8>,
2277 pub prepared: PreparedExactObject,
2278 pub initial_ack_ref: StoreAckRef,
2279 pub initial_ack: ExactProtocolObject<StoreAck>,
2280 pub state: LocalDeviceRegistrationState,
2281}
2282
2283#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2284#[serde(rename_all = "snake_case", deny_unknown_fields)]
2285pub(crate) enum LocalDeviceRegistrationState {
2286 Prepared,
2287 Created,
2288 Activated {
2289 authority: crate::sync::store_commit::StoreDeviceRegistrationActivation,
2290 },
2291 Retired {
2292 authority: crate::sync::store_commit::StoreDeviceRegistrationActivation,
2293 retirement: crate::sync::store_commit::StoreDeviceSelfRetirementRef,
2294 },
2295}
2296
2297type PreparedLocalDeviceRegistrationRow =
2298 (String, String, Vec<u8>, String, String, Vec<u8>, String);
2299type LocalDeviceRegistrationJournalRow = (
2300 String,
2301 String,
2302 Vec<u8>,
2303 String,
2304 String,
2305 Vec<u8>,
2306 String,
2307 String,
2308);
2309
2310impl DurableDeviceRegistration {
2311 pub(crate) fn is_activated(&self) -> bool {
2312 matches!(self.state, LocalDeviceRegistrationState::Activated { .. })
2313 }
2314
2315 pub(crate) fn is_retired(&self) -> bool {
2316 matches!(self.state, LocalDeviceRegistrationState::Retired { .. })
2317 }
2318}
2319
2320#[derive(Debug, Clone)]
2321pub(crate) struct DurableMembershipMutation {
2322 pub intent_hash: ObjectHash,
2323 pub plan_bytes: Vec<u8>,
2324 pub progress_bytes: Vec<u8>,
2325}
2326
2327#[derive(Debug, Clone)]
2328pub(crate) struct DurableSnapshotPublication {
2329 pub reference: StoreSnapshotRef,
2330 pub meta: ExactProtocolObject<SnapshotMeta>,
2331 pub image: ExactProtocolObject<Vec<u8>>,
2332 pub blobs: Vec<PreparedSnapshotBlob>,
2333}
2334
2335#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2336#[serde(deny_unknown_fields)]
2337pub(crate) struct PreparedSnapshotBlob {
2338 pub bindings: Vec<RowBlobLocatorBinding>,
2339 pub authority: crate::sync::audience_package::PackageAudience,
2340 pub remote: RemoteObjectRecord,
2341 pub spool_path: Option<PathBuf>,
2342}
2343
2344#[derive(Debug, Clone)]
2345pub(crate) struct PublishedStoreSnapshot {
2346 pub reference: StoreSnapshotRef,
2347 pub successor_slot: crate::storage::cloud::ObjectSlot,
2348 pub meta: SnapshotMeta,
2349}
2350
2351pub(crate) struct StoreWritePreparation {
2352 pub write_id: WriteId,
2353 pub remote_objects: Vec<RemoteObjectRecord>,
2354 pub audiences: PreparedAudienceObjects,
2355 pub commit: PreparedProtocolObject<StoreBatchCommit>,
2356 pub head: PreparedProtocolObject<StoreDeviceHead>,
2357 pub local_cleanup: StoreBatchLocalCleanup,
2358 pub completion: StoreBatchCompletion,
2359}
2360
2361pub(crate) struct MergeCandidateAbandonmentPreparation {
2362 pub write_id: WriteId,
2363 pub commit: PreparedProtocolObject<StoreBatchCommit>,
2364 pub head: PreparedProtocolObject<StoreDeviceHead>,
2365}
2366
2367pub(crate) struct SerialCandidateAbandonmentPreparation {
2368 pub branch_id: PendingBranchId,
2369 pub candidate: StoreBatchCommitRef,
2370 pub commit: PreparedProtocolObject<StoreBatchCommit>,
2371 pub head: StoreSerialHead,
2372 pub original_head_bytes: Vec<u8>,
2373}
2374
2375pub(crate) struct SerialStoreWritePreparation {
2376 pub branch_id: PendingBranchId,
2377 pub base: Option<StoreBatchCommitRef>,
2378 pub base_head_bytes: Option<Vec<u8>>,
2379 pub base_head_version: Option<VersionToken>,
2380 pub writes: Vec<SerialStoreWritePreparationEntry>,
2381 pub head: StoreSerialHead,
2382}
2383
2384pub(crate) struct SerialStoreWritePreparationEntry {
2385 pub write_id: WriteId,
2386 pub remote_objects: Vec<RemoteObjectRecord>,
2387 pub audiences: PreparedAudienceObjects,
2388 pub commit: PreparedProtocolObject<StoreBatchCommit>,
2389 pub local_cleanup: StoreBatchLocalCleanup,
2390 pub completion: StoreBatchCompletion,
2391}
2392
2393#[derive(Debug, Clone, PartialEq, Eq)]
2394pub(crate) struct CandidateCleanupObject {
2395 pub(crate) object: ExactObjectRef,
2396}
2397
2398#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2399#[serde(rename_all = "snake_case", deny_unknown_fields)]
2400enum PreparedStoreWriteState {
2401 MergeConcurrent {
2402 commit: DurablePreparedProtocolObject,
2403 head: DurablePreparedProtocolObject,
2404 local_cleanup: StoreBatchLocalCleanup,
2405 completion: StoreBatchCompletion,
2406 },
2407 MergeAbandonment {
2408 candidate_commit: DurablePreparedProtocolObject,
2409 candidate_head: DurablePreparedProtocolObject,
2410 authority_commit: DurablePreparedProtocolObject,
2411 authority_head: DurablePreparedProtocolObject,
2412 outcome: MergeAbandonmentOutcome,
2413 local_cleanup: StoreBatchLocalCleanup,
2414 completion: StoreBatchCompletion,
2415 },
2416 SerialPreparing,
2417 Serial {
2418 base_head_bytes: Option<Vec<u8>>,
2419 base_head_version: Option<VersionToken>,
2420 commit: DurablePreparedProtocolObject,
2421 tip_head_bytes: Option<Vec<u8>>,
2422 local_cleanup: StoreBatchLocalCleanup,
2423 completion: StoreBatchCompletion,
2424 },
2425}
2426
2427#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2428#[serde(rename_all = "snake_case", deny_unknown_fields)]
2429enum MergeAbandonmentOutcome {
2430 Prepared,
2431 Accepted {
2432 authority: StoreBatchCommitRef,
2433 },
2434 Lost {
2435 winner_commit: StoreBatchCommitRef,
2436 winner_head: crate::sync::store_commit::StoreDeviceHeadRef,
2437 },
2438}
2439
2440#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2441#[serde(deny_unknown_fields)]
2442struct DurableSerialCandidateAbandonment {
2443 branch_id: PendingBranchId,
2444 base: Option<StoreBatchCommitRef>,
2445 base_head: VersionedObject,
2446 candidate: StoreBatchCommitRef,
2447 commit: DurablePreparedProtocolObject,
2448 head_bytes: Vec<u8>,
2449 original_head_bytes: Vec<u8>,
2450}
2451
2452struct PreparedSerialCandidate {
2453 commit: StoreBatchCommit,
2454 reference: StoreBatchCommitRef,
2455 canonical_signed_bytes: Vec<u8>,
2456}
2457
2458struct PreparedMergeCandidate {
2459 commit: StoreBatchCommit,
2460 reference: StoreBatchCommitRef,
2461 canonical_signed_bytes: Vec<u8>,
2462 head: StoreDeviceHead,
2463 head_prepared: PreparedExactObject,
2464}
2465
2466fn parse_prepared_merge_candidate_on(
2467 conn: &Connection,
2468 prepared: &PreparedStoreWriteState,
2469) -> Result<Option<PreparedMergeCandidate>, DbError> {
2470 let (commit, head) = match prepared {
2471 PreparedStoreWriteState::MergeConcurrent { commit, head, .. } => (commit, head),
2472 PreparedStoreWriteState::MergeAbandonment {
2473 candidate_commit,
2474 candidate_head,
2475 ..
2476 } => (candidate_commit, candidate_head),
2477 PreparedStoreWriteState::SerialPreparing | PreparedStoreWriteState::Serial { .. } => {
2478 return Ok(None);
2479 }
2480 };
2481 parse_prepared_merge_candidate_parts_on(conn, commit, head).map(Some)
2482}
2483
2484fn parse_prepared_merge_candidate_parts_on(
2485 conn: &Connection,
2486 commit: &DurablePreparedProtocolObject,
2487 head: &DurablePreparedProtocolObject,
2488) -> Result<PreparedMergeCandidate, DbError> {
2489 let root = required_store_root_authority_on(conn)?;
2490 let unverified: StoreBatchCommit = serde_json::from_slice(&commit.semantic_bytes)
2491 .map_err(|error| DbError::Message(format!("signed Merge candidate: {error}")))?;
2492 let registration =
2493 load_activated_registration_on(conn, &root, &unverified.author_registration)?;
2494 let coord = StoreCommitCoord::MergeConcurrent {
2495 stream_id: AuthorStreamId::store_announcements(&root, &unverified.author_registration),
2496 sequence: unverified.seq(),
2497 };
2498 let value = StoreBatchCommit::parse_at(
2499 &commit.semantic_bytes,
2500 root.store_root_hash,
2501 &coord,
2502 ®istration,
2503 )
2504 .map_err(|error| DbError::Message(format!("verify Merge candidate: {error}")))?;
2505 let reference =
2506 StoreBatchCommitRef::from_commit(&value, coord, commit.prepared.reference().clone())
2507 .map_err(|error| DbError::Message(error.to_string()))?;
2508 let head_value = StoreDeviceHead::parse_at(
2509 &head.semantic_bytes,
2510 root.store_root_hash,
2511 ®istration,
2512 &reference,
2513 )
2514 .map_err(|error| DbError::Message(format!("verify Merge candidate head: {error}")))?;
2515 Ok(PreparedMergeCandidate {
2516 commit: value,
2517 reference,
2518 canonical_signed_bytes: commit.semantic_bytes.clone(),
2519 head: head_value,
2520 head_prepared: head.prepared.clone(),
2521 })
2522}
2523
2524fn parse_prepared_merge_publication_on(
2525 conn: &Connection,
2526 prepared: &PreparedStoreWriteState,
2527) -> Result<Option<PreparedMergeCandidate>, DbError> {
2528 match prepared {
2529 PreparedStoreWriteState::MergeConcurrent { commit, head, .. } => {
2530 parse_prepared_merge_candidate_parts_on(conn, commit, head).map(Some)
2531 }
2532 PreparedStoreWriteState::MergeAbandonment {
2533 authority_commit,
2534 authority_head,
2535 ..
2536 } => parse_prepared_merge_candidate_parts_on(conn, authority_commit, authority_head)
2537 .map(Some),
2538 PreparedStoreWriteState::SerialPreparing | PreparedStoreWriteState::Serial { .. } => {
2539 Ok(None)
2540 }
2541 }
2542}
2543
2544fn parse_prepared_serial_candidate(raw: &str) -> Result<Option<PreparedSerialCandidate>, DbError> {
2545 let prepared: PreparedStoreWriteState = serde_json::from_str(raw)
2546 .map_err(|error| DbError::Message(format!("prepared Serial candidate: {error}")))?;
2547 let PreparedStoreWriteState::Serial { commit, .. } = prepared else {
2548 return match prepared {
2549 PreparedStoreWriteState::SerialPreparing => Ok(None),
2550 PreparedStoreWriteState::MergeConcurrent { .. } => Err(DbError::Message(
2551 "MergeConcurrent publication reached Serial candidate state".to_string(),
2552 )),
2553 PreparedStoreWriteState::MergeAbandonment { .. } => Err(DbError::Message(
2554 "Merge abandonment reached Serial candidate state".to_string(),
2555 )),
2556 PreparedStoreWriteState::Serial { .. } => unreachable!(),
2557 };
2558 };
2559 let value: StoreBatchCommit = serde_json::from_slice(&commit.semantic_bytes)
2560 .map_err(|error| DbError::Message(format!("signed Serial candidate: {error}")))?;
2561 let reference = StoreBatchCommitRef::from_commit(
2562 &value,
2563 StoreCommitCoord::Serial {
2564 sequence: value.seq(),
2565 },
2566 commit.prepared.reference().clone(),
2567 )
2568 .map_err(|error| DbError::Message(error.to_string()))?;
2569 Ok(Some(PreparedSerialCandidate {
2570 commit: value,
2571 reference,
2572 canonical_signed_bytes: commit.semantic_bytes,
2573 }))
2574}
2575
2576fn begin_merge_candidate_nonactivation_on(
2577 conn: &Connection,
2578 write_id: &WriteId,
2579 candidate: &PreparedMergeCandidate,
2580 winner_head: crate::sync::store_commit::StoreDeviceHeadRef,
2581 include_indexed_objects: bool,
2582) -> Result<(), DbError> {
2583 let nonactivation = crate::sync::remote_object::CandidateNonactivation {
2584 candidate: crate::sync::store_commit::StoreBatchCommitDeletionTarget {
2585 coord: candidate.reference.coord.clone(),
2586 object: candidate.reference.object.clone(),
2587 canonical_signed_bytes: candidate.canonical_signed_bytes.clone(),
2588 },
2589 proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { winner_head },
2590 };
2591 let mut object_ids = if include_indexed_objects {
2592 let mut statement = conn
2593 .prepare(
2594 "SELECT remote_object_id FROM store_write_packages WHERE write_id = ?1
2595 UNION
2596 SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1
2597 ORDER BY remote_object_id",
2598 )
2599 .map_err(DbError::from)?;
2600 let object_ids = statement
2601 .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
2602 .map_err(DbError::from)?
2603 .collect::<Result<Vec<_>, _>>()
2604 .map_err(DbError::from)?;
2605 drop(statement);
2606 object_ids
2607 } else {
2608 Vec::new()
2609 };
2610 object_ids.push(remote_object_id(candidate.head_prepared.reference()).to_string());
2611 for encoded in object_ids {
2612 let object_id: ObjectHash = encoded.parse().map_err(|error| {
2613 DbError::Message(format!("Merge conflict remote object id: {error}"))
2614 })?;
2615 let mut remote = load_remote_object_on(conn, object_id)?;
2616 remote
2617 .begin_candidate_nonactivation(nonactivation.clone())
2618 .map_err(|error| {
2619 DbError::Message(format!(
2620 "record Merge candidate nonactivation for {object_id}: {error}"
2621 ))
2622 })?;
2623 update_remote_object_on(conn, object_id, &remote)?;
2624 }
2625 let commit_object_id = remote_object_id(&candidate.reference.object);
2626 let mut remote = load_remote_object_on(conn, commit_object_id)?;
2627 remote
2628 .begin_candidate_nonactivation(nonactivation)
2629 .map_err(|error| {
2630 DbError::Message(format!(
2631 "record Merge candidate commit nonactivation for {commit_object_id}: {error}"
2632 ))
2633 })?;
2634 update_remote_object_on(conn, commit_object_id, &remote)
2635}
2636
2637fn merge_candidate_cleanup_targets_on(
2638 conn: &Connection,
2639 write_id: &WriteId,
2640 candidate: &PreparedMergeCandidate,
2641 include_indexed_objects: bool,
2642) -> Result<Vec<CandidateCleanupObject>, DbError> {
2643 let commit_remote = load_remote_object_on(conn, remote_object_id(&candidate.reference.object))?;
2644 if !matches!(
2645 &commit_remote,
2646 RemoteObjectRecord::CandidateCommit(record)
2647 if matches!(
2648 &record.state,
2649 crate::sync::remote_object::CandidateCommitState::CleanupPending {
2650 proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { .. }
2651 } | crate::sync::remote_object::CandidateCommitState::AbsentVerified {
2652 proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { .. }
2653 }
2654 )
2655 ) {
2656 return Err(DbError::Message(
2657 "Merge candidate has no durable winner proof".to_string(),
2658 ));
2659 }
2660 let mut cleanup = BTreeMap::new();
2661 if include_indexed_objects {
2662 let mut statement = conn
2663 .prepare(
2664 "SELECT remote_object_id FROM store_write_packages WHERE write_id = ?1
2665 UNION
2666 SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1",
2667 )
2668 .map_err(DbError::from)?;
2669 let encoded = statement
2670 .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
2671 .map_err(DbError::from)?
2672 .collect::<Result<Vec<_>, _>>()
2673 .map_err(DbError::from)?;
2674 drop(statement);
2675 for encoded in encoded {
2676 let object_id: ObjectHash = encoded.parse().map_err(|error| {
2677 DbError::Message(format!("Merge cleanup remote object id: {error}"))
2678 })?;
2679 let remote = load_remote_object_on(conn, object_id)?;
2680 if let Some(object) = remote.cleanup_target() {
2681 cleanup.insert(
2682 object.clone(),
2683 CandidateCleanupObject {
2684 object: object.clone(),
2685 },
2686 );
2687 } else if !remote
2688 .candidate_cleanup_complete(&candidate.reference)
2689 .map_err(|error| DbError::Message(format!("Merge cleanup {object_id}: {error}")))?
2690 {
2691 return Err(DbError::Message(format!(
2692 "Merge candidate object {object_id} has no cleanup transition"
2693 )));
2694 }
2695 }
2696 }
2697 let head_remote =
2698 load_remote_object_on(conn, remote_object_id(candidate.head_prepared.reference()))?;
2699 if !head_remote
2700 .candidate_cleanup_complete(&candidate.reference)
2701 .map_err(|error| DbError::Message(format!("Merge cleanup head: {error}")))?
2702 {
2703 return Err(DbError::Message(
2704 "Merge candidate head absence is not verified".to_string(),
2705 ));
2706 }
2707 let mut targets = Vec::new();
2708 for object in candidate_manifest_exact_objects(&candidate.commit) {
2709 if let Some(target) = cleanup.remove(object) {
2710 targets.push(target);
2711 }
2712 }
2713 if !cleanup.is_empty() {
2714 return Err(DbError::Message(
2715 "Merge cleanup contains an object outside the signed candidate manifest".to_string(),
2716 ));
2717 }
2718 if let Some(object) = commit_remote.cleanup_target() {
2719 targets.push(CandidateCleanupObject {
2720 object: object.clone(),
2721 });
2722 } else if !commit_remote
2723 .candidate_cleanup_complete(&candidate.reference)
2724 .map_err(|error| DbError::Message(format!("Merge cleanup commit: {error}")))?
2725 {
2726 return Err(DbError::Message(
2727 "Merge candidate commit cleanup is incomplete".to_string(),
2728 ));
2729 }
2730 Ok(targets)
2731}
2732
2733fn remove_cleaned_merge_authority_on(
2734 tx: &rusqlite::Transaction<'_>,
2735 authority: &PreparedMergeCandidate,
2736) -> Result<(), DbError> {
2737 for object in [
2738 authority.reference.object.clone(),
2739 authority.head_prepared.reference().clone(),
2740 ] {
2741 let object_id = remote_object_id(&object);
2742 let remote = load_remote_object_on(tx, object_id)?;
2743 if !remote
2744 .candidate_cleanup_complete(&authority.reference)
2745 .map_err(|error| {
2746 DbError::Message(format!(
2747 "validate abandoned authority cleanup for {object_id}: {error}"
2748 ))
2749 })?
2750 {
2751 return Err(DbError::Message(
2752 "losing Merge abandonment cleanup is incomplete".to_string(),
2753 ));
2754 }
2755 let removed = tx
2756 .execute(
2757 "DELETE FROM remote_objects WHERE object_id = ?1",
2758 [object_id.to_string()],
2759 )
2760 .map_err(DbError::from)?;
2761 if removed != 1 {
2762 return Err(DbError::Message(format!(
2763 "abandoned authority object {object_id} disappeared during removal"
2764 )));
2765 }
2766 }
2767 Ok(())
2768}
2769
2770#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2771#[serde(deny_unknown_fields)]
2772struct DurablePreparedProtocolObject {
2773 semantic_bytes: Vec<u8>,
2774 prepared: PreparedExactObject,
2775}
2776
2777#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2778#[serde(deny_unknown_fields)]
2779pub(crate) struct StoreBatchLocalCleanup {
2780 pub drops: Vec<crate::sync::service::DeferredLocalBlobDrop>,
2781}
2782
2783#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2784#[serde(deny_unknown_fields)]
2785pub(crate) struct StoreBatchCompletion {}
2786
2787fn validate_host_synced_tables(
2788 conn: &Connection,
2789 synced_tables: &[SyncedTable],
2790) -> Result<(), DbError> {
2791 let mut namespace_owner: HashMap<&str, &str> = HashMap::new();
2796 let mut table_by_sqlite_name: HashMap<String, &str> = HashMap::new();
2797 for table in synced_tables {
2798 let name = table.name();
2799 if name.is_empty() {
2800 return Err(DbError::Message(
2801 "synced table name must not be empty".to_string(),
2802 ));
2803 }
2804 if is_reserved_table_name(name) {
2805 return Err(DbError::Message(format!(
2806 "synced table {name:?} is reserved by coven"
2807 )));
2808 }
2809 let sqlite_name = name.to_ascii_lowercase();
2810 if let Some(prior) = table_by_sqlite_name.insert(sqlite_name, name) {
2811 return Err(DbError::Message(format!(
2812 "synced tables {prior:?} and {name:?} are declared as the same SQLite table more than once"
2813 )));
2814 }
2815 if let Some(live_name) = canonical_table_name(conn, name)? {
2816 if live_name != name {
2817 return Err(DbError::Message(format!(
2818 "synced table {name:?} does not use the live schema's exact spelling {live_name:?}"
2819 )));
2820 }
2821 }
2822 validate_synced_table_contract(conn, name)?;
2823 validate_existing_row_identities(conn, table)?;
2824 if let Some(decl) = table.blob() {
2825 let namespace = decl.namespace.as_str();
2826 if let Some(prior) = namespace_owner.insert(namespace, name) {
2827 return Err(DbError::Message(format!(
2828 "synced tables {prior:?} and {name:?} both declare blob namespace \
2829 {namespace:?}; a namespace must be owned by exactly one table"
2830 )));
2831 }
2832 }
2833 }
2834 Ok(())
2835}
2836
2837fn canonical_table_name(conn: &Connection, table: &str) -> Result<Option<String>, DbError> {
2842 conn.query_row(
2843 "SELECT name FROM main.sqlite_schema \
2844 WHERE type = 'table' AND name = ?1 COLLATE NOCASE",
2845 [table],
2846 |row| row.get(0),
2847 )
2848 .optional()
2849 .map_err(DbError::from)
2850}
2851
2852fn validate_existing_row_identities(conn: &Connection, table: &SyncedTable) -> Result<(), DbError> {
2853 if table.row_identity() == crate::sync::session::RowIdentity::SharedKey {
2854 return Ok(());
2855 }
2856 let sql = format!(
2857 "SELECT id FROM {}",
2858 crate::sync::session::quote_ident(table.name())
2859 );
2860 let mut statement = conn.prepare(&sql).map_err(DbError::from)?;
2861 let ids = statement
2862 .query_map([], |row| row.get::<_, String>(0))
2863 .map_err(DbError::from)?;
2864 for id in ids {
2865 let id = id.map_err(DbError::from)?;
2866 crate::sync::session::validate_row_identity(table.name(), table.row_identity(), &id)
2867 .map_err(|error| DbError::Message(error.to_string()))?;
2868 }
2869 Ok(())
2870}
2871
2872struct ColumnInfo {
2877 position: i64,
2878 name: String,
2879 declared_type: String,
2880 not_null: bool,
2881 pk: i64,
2882}
2883
2884fn validate_synced_table_contract(conn: &Connection, table: &str) -> Result<(), DbError> {
2891 match table_is_strict(conn, table)? {
2892 None => {
2893 return Err(DbError::Message(format!(
2894 "synced table {table:?} is declared in `synced_tables` but no migration \
2895 creates it — add a `CREATE TABLE {table} (...) STRICT` to the schema \
2896 migrations, or remove the declaration"
2897 )));
2898 }
2899 Some(false) => {
2900 return Err(DbError::Message(format!(
2901 "synced table {table:?} is not declared STRICT; the sync contract assumes typed \
2902 columns (apply preserves storage classes peer-to-peer, LWW arbitration renders \
2903 values to strings for comparison), which STRICT enforces at the insert — declare \
2904 it STRICT: `CREATE TABLE {table} (...) STRICT`"
2905 )));
2906 }
2907 Some(true) => {}
2908 }
2909
2910 let sql = format!(
2911 "PRAGMA table_info({})",
2912 crate::sync::session::quote_ident(table)
2913 );
2914 let mut stmt = conn.prepare(&sql).map_err(DbError::from)?;
2915 let mut columns = Vec::new();
2916 let rows = stmt
2917 .query_map([], |row| {
2918 Ok(ColumnInfo {
2919 position: row.get::<_, i64>(0)?,
2920 name: row.get::<_, String>(1)?,
2921 declared_type: row.get::<_, String>(2)?,
2922 not_null: row.get::<_, i64>(3)? != 0,
2923 pk: row.get::<_, i64>(5)?,
2924 })
2925 })
2926 .map_err(DbError::from)?;
2927 for row in rows {
2928 columns.push(row.map_err(DbError::from)?);
2929 }
2930
2931 let pk_columns: Vec<&ColumnInfo> = columns.iter().filter(|c| c.pk > 0).collect();
2932 let pk = match pk_columns.as_slice() {
2933 [single] => *single,
2934 [] => {
2935 return Err(DbError::Message(format!(
2936 "synced table {table:?} has no primary key; the contract requires a single \
2937 `id` TEXT primary key at column 0"
2938 )))
2939 }
2940 _ => {
2941 let names: Vec<&str> = pk_columns.iter().map(|c| c.name.as_str()).collect();
2942 return Err(DbError::Message(format!(
2943 "synced table {table:?} has a composite primary key {names:?}; the contract \
2944 requires a single `id` TEXT primary key at column 0"
2945 )));
2946 }
2947 };
2948 if pk.name != "id" {
2949 return Err(DbError::Message(format!(
2950 "synced table {table:?} primary key is {:?}, not `id`; the contract requires the \
2951 primary key to be the `id` column",
2952 pk.name
2953 )));
2954 }
2955 if pk.position != 0 {
2956 return Err(DbError::Message(format!(
2957 "synced table {table:?} primary key `id` is at column {}, not column 0; the \
2958 contract requires `id` to be the first column",
2959 pk.position
2960 )));
2961 }
2962 if !declared_as_text(&pk.declared_type) {
2963 return Err(DbError::Message(format!(
2964 "synced table {table:?} primary key `id` is declared {:?}, not TEXT; the contract \
2965 requires an `id` TEXT primary key",
2966 pk.declared_type
2967 )));
2968 }
2969
2970 let updated_at = columns
2971 .iter()
2972 .find(|c| c.name == "_updated_at")
2973 .ok_or_else(|| {
2974 DbError::Message(format!(
2975 "synced table {table:?} has no `_updated_at` column; the contract requires \
2976 `_updated_at TEXT NOT NULL`"
2977 ))
2978 })?;
2979 if !declared_as_text(&updated_at.declared_type) {
2980 return Err(DbError::Message(format!(
2981 "synced table {table:?} column `_updated_at` is declared {:?}, not TEXT; the \
2982 contract requires `_updated_at TEXT NOT NULL`",
2983 updated_at.declared_type
2984 )));
2985 }
2986 if !updated_at.not_null {
2987 return Err(DbError::Message(format!(
2988 "synced table {table:?} column `_updated_at` is nullable; the contract requires \
2989 `_updated_at TEXT NOT NULL`"
2990 )));
2991 }
2992
2993 Ok(())
2994}
2995
2996fn declared_as_text(declared_type: &str) -> bool {
3000 declared_type.eq_ignore_ascii_case("TEXT")
3001}
3002
3003fn table_is_strict(conn: &Connection, table: &str) -> Result<Option<bool>, DbError> {
3010 let sql = format!(
3011 "PRAGMA table_list({})",
3012 crate::sync::session::quote_ident(table)
3013 );
3014 let mut stmt = conn.prepare(&sql).map_err(DbError::from)?;
3015 let rows = stmt
3016 .query_map([], |row| {
3017 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(5)?))
3018 })
3019 .map_err(DbError::from)?;
3020 for row in rows {
3021 let (schema, strict) = row.map_err(DbError::from)?;
3022 if schema == "main" {
3023 return Ok(Some(strict != 0));
3024 }
3025 }
3026 Ok(None)
3027}
3028
3029struct PreparedCircleOperationRow {
3030 operation_id: String,
3031 circle_id: String,
3032 payload: Vec<u8>,
3033}
3034
3035impl PreparedCircleOperationRow {
3036 fn from_journal(
3037 journal: crate::sync::circle_ops::CircleOperationJournal,
3038 ) -> Result<Self, DbError> {
3039 let operation_id = journal.operation_id.clone();
3040 let circle_id = journal.circle_id().to_string();
3041 let payload = serde_json::to_vec(&journal).map_err(|error| {
3042 DbError::Message(format!("serialize circle operation journal: {error}"))
3043 })?;
3044 Ok(Self {
3045 operation_id,
3046 circle_id,
3047 payload,
3048 })
3049 }
3050}
3051
3052impl Database {
3053 pub(crate) fn id_provider(&self) -> &dyn crate::id_provider::IdProvider {
3054 self.state.ids.as_ref()
3055 }
3056
3057 #[doc(hidden)]
3058 pub fn new_write_id(&self) -> WriteId {
3059 WriteId::from_generated(self.state.ids.new_id())
3060 }
3061
3062 fn notify_write_status_in(
3063 statuses: &Arc<std::sync::Mutex<HashMap<WriteId, tokio::sync::watch::Sender<WriteStatus>>>>,
3064 write_id: &WriteId,
3065 status: WriteStatus,
3066 ) {
3067 let senders = statuses.lock().expect("write status mutex poisoned");
3068 if let Some(sender) = senders.get(write_id) {
3069 sender.send_replace(status);
3070 }
3071 }
3072
3073 fn set_write_status_on(
3074 conn: &Connection,
3075 write_id: &WriteId,
3076 status: &WriteStatus,
3077 ) -> Result<(), DbError> {
3078 let status = serde_json::to_string(status)
3079 .map_err(|error| DbError::Message(format!("serialize write status: {error}")))?;
3080 let updated = conn
3081 .execute(
3082 "UPDATE store_writes SET status = ?2 WHERE write_id = ?1",
3083 rusqlite::params![write_id.as_str(), status],
3084 )
3085 .map_err(DbError::from)?;
3086 if updated != 1 {
3087 return Err(DbError::Message(format!("write {write_id} does not exist")));
3088 }
3089 Ok(())
3090 }
3091
3092 pub(crate) async fn set_write_status(
3093 &self,
3094 write_id: &WriteId,
3095 status: WriteStatus,
3096 ) -> Result<(), DbError> {
3097 let stored_id = write_id.clone();
3098 let stored_status = status.clone();
3099 let statuses = self.state.write_statuses.clone();
3100 self.call(move |conn| {
3101 Self::set_write_status_on(conn, &stored_id, &stored_status)?;
3102 Self::notify_write_status_in(&statuses, &stored_id, stored_status);
3103 Ok(())
3104 })
3105 .await
3106 }
3107
3108 pub async fn write_status(&self, write_id: &WriteId) -> Result<WriteStatus, DbError> {
3109 let write_id = write_id.clone();
3110 self.call(move |conn| {
3111 let raw: String = conn
3112 .query_row(
3113 "SELECT status FROM store_writes WHERE write_id = ?1",
3114 [write_id.as_str()],
3115 |row| row.get(0),
3116 )
3117 .map_err(DbError::from)?;
3118 serde_json::from_str(&raw)
3119 .map_err(|error| DbError::Message(format!("write {write_id} status: {error}")))
3120 })
3121 .await
3122 }
3123
3124 pub async fn pending_writes(&self) -> Result<Vec<PendingWrite>, DbError> {
3125 self.call(move |conn| {
3126 let mut statement = conn
3127 .prepare(
3128 "SELECT write_id, status, affected_rows FROM store_writes
3129 WHERE status IN ('\"pending\"', '\"publishing\"')
3130 OR json_extract(status, '$.blocked') IS NOT NULL
3131 OR json_extract(status, '$.conflict') IS NOT NULL
3132 ORDER BY ordinal",
3133 )
3134 .map_err(DbError::from)?;
3135 let rows = statement
3136 .query_map([], |row| {
3137 Ok((
3138 row.get::<_, String>(0)?,
3139 row.get::<_, String>(1)?,
3140 row.get::<_, String>(2)?,
3141 ))
3142 })
3143 .map_err(DbError::from)?;
3144 rows.map(|row| {
3145 let (write_id, status, affected_rows) = row.map_err(DbError::from)?;
3146 Ok(PendingWrite {
3147 write_id: WriteId::from_generated(write_id),
3148 status: serde_json::from_str(&status).map_err(|error| {
3149 DbError::Message(format!("pending write status: {error}"))
3150 })?,
3151 affected_rows: serde_json::from_str(&affected_rows).map_err(|error| {
3152 DbError::Message(format!("pending affected rows: {error}"))
3153 })?,
3154 })
3155 })
3156 .collect()
3157 })
3158 .await
3159 }
3160
3161 pub async fn blocked_writes(&self) -> Result<Vec<PendingWrite>, DbError> {
3163 Ok(self
3164 .pending_writes()
3165 .await?
3166 .into_iter()
3167 .filter(|write| matches!(write.status, WriteStatus::Blocked(_)))
3168 .collect())
3169 }
3170
3171 pub async fn retry_blocked_write(&self, write_id: &WriteId) -> Result<Vec<WriteId>, DbError> {
3175 let write_id = write_id.clone();
3176 let statuses = self.state.write_statuses.clone();
3177 self.call(move |conn| {
3178 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
3179 let (raw_status, raw_base, prepared): (String, String, Option<String>) = tx
3180 .query_row(
3181 "SELECT status, base, prepared FROM store_writes WHERE write_id = ?1",
3182 [write_id.as_str()],
3183 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
3184 )
3185 .map_err(DbError::from)?;
3186 let status: WriteStatus = serde_json::from_str(&raw_status)
3187 .map_err(|error| DbError::Message(format!("blocked write {write_id} status: {error}")))?;
3188 if !matches!(status, WriteStatus::Blocked(_)) {
3189 return Err(DbError::Message(format!("write {write_id} is not blocked")));
3190 }
3191 let base: StoreWriteBase = serde_json::from_str(&raw_base)
3192 .map_err(|error| DbError::Message(format!("blocked write {write_id} base: {error}")))?;
3193
3194 let mut retried = Vec::new();
3195 match base {
3196 StoreWriteBase::MergeConcurrent { .. } => {
3197 if let Some(raw_prepared) = prepared.as_deref() {
3198 let prepared: PreparedStoreWriteState = serde_json::from_str(raw_prepared)
3199 .map_err(|error| {
3200 DbError::Message(format!("blocked write {write_id} preparation: {error}"))
3201 })?;
3202 if !matches!(prepared, PreparedStoreWriteState::MergeConcurrent { .. }) {
3203 return Err(DbError::Message(format!(
3204 "blocked MergeConcurrent write {write_id} has Serial preparation"
3205 )));
3206 }
3207 let candidate = parse_prepared_merge_candidate_on(&tx, &prepared)?
3208 .expect("matched Merge preparation")
3209 .reference;
3210 let remote =
3211 load_remote_object_on(&tx, remote_object_id(&candidate.object))?;
3212 if matches!(
3213 remote,
3214 RemoteObjectRecord::CandidateCommit(
3215 crate::sync::remote_object::CandidateCommitRecord {
3216 state:
3217 crate::sync::remote_object::CandidateCommitState::CleanupPending {
3218 proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { .. }
3219 }
3220 | crate::sync::remote_object::CandidateCommitState::AbsentVerified {
3221 proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { .. }
3222 },
3223 ..
3224 }
3225 )
3226 ) {
3227 return Err(DbError::Message(format!(
3228 "Merge write {write_id} has an irreversible winner and cannot be retried"
3229 )));
3230 }
3231 }
3232 let next = if prepared.is_some() {
3233 WriteStatus::Publishing
3234 } else {
3235 WriteStatus::Pending
3236 };
3237 let next_json = serde_json::to_string(&next)
3238 .map_err(|error| DbError::Message(format!("serialize retry status: {error}")))?;
3239 let updated = tx
3240 .execute(
3241 "UPDATE store_writes SET status = ?2
3242 WHERE write_id = ?1 AND json_extract(status, '$.blocked') IS NOT NULL",
3243 rusqlite::params![write_id.as_str(), next_json],
3244 )
3245 .map_err(DbError::from)?;
3246 if updated != 1 {
3247 return Err(DbError::Message(format!(
3248 "blocked write {write_id} changed during retry"
3249 )));
3250 }
3251 retried.push((write_id, next));
3252 }
3253 StoreWriteBase::Serial {
3254 branch_id,
3255 base: branch_base,
3256 } => {
3257 if prepared.is_some() {
3258 return Err(DbError::Message(format!(
3259 "blocked Serial branch {} retains publication preparation",
3260 branch_id.first_write_id()
3261 )));
3262 }
3263 let expected_base = StoreWriteBase::Serial {
3264 branch_id: branch_id.clone(),
3265 base: branch_base,
3266 };
3267 let mut statement = tx
3268 .prepare(
3269 "SELECT write_id, status, base, prepared FROM store_writes
3270 WHERE base = ?1
3271 AND status != '\"local_only\"'
3272 AND json_extract(status, '$.published') IS NULL
3273 AND json_extract(status, '$.resolved') IS NULL
3274 ORDER BY ordinal",
3275 )
3276 .map_err(DbError::from)?;
3277 let rows = statement
3278 .query_map([&raw_base], |row| {
3279 Ok((
3280 row.get::<_, String>(0)?,
3281 row.get::<_, String>(1)?,
3282 row.get::<_, String>(2)?,
3283 row.get::<_, Option<String>>(3)?,
3284 ))
3285 })
3286 .map_err(DbError::from)?;
3287 let mut branch_write_ids = Vec::new();
3288 for row in rows {
3289 let (stored_id, raw_status, raw_base, prepared) =
3290 row.map_err(DbError::from)?;
3291 let stored_base: StoreWriteBase = serde_json::from_str(&raw_base)
3292 .map_err(|error| DbError::Message(format!("Serial retry base: {error}")))?;
3293 if stored_base != expected_base {
3294 return Err(DbError::Message(
3295 "Serial database contains more than one unresolved branch"
3296 .to_string(),
3297 ));
3298 }
3299 let stored_status: WriteStatus = serde_json::from_str(&raw_status)
3300 .map_err(|error| DbError::Message(format!("Serial retry status: {error}")))?;
3301 if !matches!(stored_status, WriteStatus::Pending | WriteStatus::Blocked(_))
3302 || prepared.is_some()
3303 {
3304 return Err(DbError::Message(format!(
3305 "Serial branch write {stored_id} is not pending or blocked without preparation"
3306 )));
3307 }
3308 if matches!(stored_status, WriteStatus::Blocked(_)) {
3309 branch_write_ids.push(WriteId::from_generated(stored_id));
3310 }
3311 }
3312 drop(statement);
3313 let pending = serde_json::to_string(&WriteStatus::Pending)
3314 .map_err(|error| DbError::Message(format!("serialize retry status: {error}")))?;
3315 for branch_write_id in branch_write_ids {
3316 let updated = tx
3317 .execute(
3318 "UPDATE store_writes SET status = ?2
3319 WHERE write_id = ?1
3320 AND json_extract(status, '$.blocked') IS NOT NULL
3321 AND prepared IS NULL",
3322 rusqlite::params![branch_write_id.as_str(), &pending],
3323 )
3324 .map_err(DbError::from)?;
3325 if updated != 1 {
3326 return Err(DbError::Message(format!(
3327 "blocked Serial write {branch_write_id} changed during retry"
3328 )));
3329 }
3330 retried.push((branch_write_id, WriteStatus::Pending));
3331 }
3332 }
3333 }
3334 tx.commit().map_err(DbError::from)?;
3335 let retried_ids = retried
3336 .iter()
3337 .map(|(write_id, _)| write_id.clone())
3338 .collect();
3339 for (write_id, status) in retried {
3340 Self::notify_write_status_in(&statuses, &write_id, status);
3341 }
3342 Ok(retried_ids)
3343 })
3344 .await
3345 }
3346
3347 pub async fn discard_blocked_write(&self, write_id: &WriteId) -> Result<Vec<WriteId>, DbError> {
3350 let write_id = write_id.clone();
3351 let synced_tables = self.synced_tables().to_vec();
3352 let statuses = self.state.write_statuses.clone();
3353 self.call(move |conn| {
3354 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
3355 let (raw_status, target_ordinal): (String, i64) = tx
3356 .query_row(
3357 "SELECT status, ordinal FROM store_writes WHERE write_id = ?1",
3358 [write_id.as_str()],
3359 |row| Ok((row.get(0)?, row.get(1)?)),
3360 )
3361 .map_err(DbError::from)?;
3362 let target_status: WriteStatus = serde_json::from_str(&raw_status)
3363 .map_err(|error| DbError::Message(format!("blocked write {write_id} status: {error}")))?;
3364 if !matches!(target_status, WriteStatus::Blocked(_)) {
3365 return Err(DbError::Message(format!("write {write_id} is not blocked")));
3366 }
3367
3368 let mut statement = tx
3369 .prepare(
3370 "SELECT write_id, status, inverse_changeset FROM store_writes
3371 WHERE ordinal >= ?1
3372 AND status != '\"local_only\"'
3373 AND json_extract(status, '$.published') IS NULL
3374 AND json_extract(status, '$.resolved') IS NULL
3375 ORDER BY ordinal",
3376 )
3377 .map_err(DbError::from)?;
3378 let rows = statement
3379 .query_map([target_ordinal], |row| {
3380 Ok((
3381 row.get::<_, String>(0)?,
3382 row.get::<_, String>(1)?,
3383 row.get::<_, Vec<u8>>(2)?,
3384 ))
3385 })
3386 .map_err(DbError::from)?;
3387 let mut discarded = Vec::new();
3388 for row in rows {
3389 let (stored_id, raw_status, inverse) = row.map_err(DbError::from)?;
3390 let status: WriteStatus = serde_json::from_str(&raw_status)
3391 .map_err(|error| DbError::Message(format!("discard write status: {error}")))?;
3392 if !matches!(status, WriteStatus::Pending | WriteStatus::Blocked(_)) {
3393 return Err(DbError::Message(format!(
3394 "write {stored_id} after blocked write {write_id} has non-discardable status {status:?}"
3395 )));
3396 }
3397 discarded.push((WriteId::from_generated(stored_id), inverse));
3398 }
3399 drop(statement);
3400 if discarded.first().map(|(stored_id, _)| stored_id) != Some(&write_id) {
3401 return Err(DbError::Message(format!(
3402 "blocked write {write_id} is absent from its unpublished suffix"
3403 )));
3404 }
3405 let schema = Arc::new(crate::sync::conflict::TableSchema::from_db(
3406 &tx,
3407 &synced_tables,
3408 )?);
3409 for (_, inverse) in discarded.iter().rev() {
3410 let inverse = crate::sync::apply::ValidatedChangeset::new(
3411 inverse,
3412 schema.clone(),
3413 )
3414 .map_err(|error| DbError::Message(format!("invalid blocked-write inverse: {error}")))?;
3415 crate::sync::apply::apply_changeset_strict_on(&tx, inverse)
3416 .map_err(|error| DbError::Message(format!("reverse blocked-write suffix: {error}")))?;
3417 }
3418 let discarded_ids: Vec<_> = discarded
3419 .into_iter()
3420 .map(|(write_id, _)| write_id)
3421 .collect();
3422 let resolution = WriteResolution::Discarded;
3423 Self::resolve_unpublished_writes_on(&tx, &discarded_ids, &resolution)?;
3424 tx.commit().map_err(DbError::from)?;
3425 let status = WriteStatus::Resolved(resolution);
3426 for discarded_id in &discarded_ids {
3427 Self::notify_write_status_in(&statuses, discarded_id, status.clone());
3428 }
3429 Ok(discarded_ids)
3430 })
3431 .await
3432 }
3433
3434 pub async fn pending_branches(&self) -> Result<Option<PendingBranch>, DbError> {
3435 self.call(|conn| {
3436 let mut statement = conn
3437 .prepare(
3438 "SELECT write_id, status, affected_rows, base FROM store_writes
3439 WHERE status IN ('\"pending\"', '\"publishing\"')
3440 OR json_extract(status, '$.blocked') IS NOT NULL
3441 OR json_extract(status, '$.conflict') IS NOT NULL
3442 ORDER BY ordinal",
3443 )
3444 .map_err(DbError::from)?;
3445 let rows = statement
3446 .query_map([], |row| {
3447 Ok((
3448 row.get::<_, String>(0)?,
3449 row.get::<_, String>(1)?,
3450 row.get::<_, String>(2)?,
3451 row.get::<_, String>(3)?,
3452 ))
3453 })
3454 .map_err(DbError::from)?;
3455 let mut records = Vec::new();
3456 for row in rows {
3457 let (write_id, status, affected_rows, base) = row.map_err(DbError::from)?;
3458 records.push((
3459 PendingWrite {
3460 write_id: WriteId::from_generated(write_id),
3461 status: serde_json::from_str(&status).map_err(|error| {
3462 DbError::Message(format!("pending write status: {error}"))
3463 })?,
3464 affected_rows: serde_json::from_str(&affected_rows).map_err(|error| {
3465 DbError::Message(format!("pending affected rows: {error}"))
3466 })?,
3467 },
3468 serde_json::from_str::<StoreWriteBase>(&base).map_err(|error| {
3469 DbError::Message(format!("pending write base: {error}"))
3470 })?,
3471 ));
3472 }
3473 drop(statement);
3474
3475 let mut conflict = None;
3476 let mut conflict_exact_base = None;
3477 for (write, base) in &records {
3478 let WriteStatus::Conflict(candidate) = &write.status else {
3479 continue;
3480 };
3481 let StoreWriteBase::Serial {
3482 branch_id,
3483 base: stored_base,
3484 } = base
3485 else {
3486 return Err(DbError::Message(
3487 "MergeConcurrent base carries a Serial conflict".to_string(),
3488 ));
3489 };
3490 let exact_base = store_serial_predecessor_on(conn, stored_base.as_ref())?;
3491 if branch_id != &candidate.branch_id || exact_base != candidate.base {
3492 return Err(DbError::Message(
3493 "Serial conflict status differs from its durable branch base".to_string(),
3494 ));
3495 }
3496 match &conflict {
3497 None => {
3498 conflict = Some(candidate.clone());
3499 conflict_exact_base = Some(stored_base.clone());
3500 }
3501 Some(existing) if existing == candidate => {}
3502 Some(_) => {
3503 return Err(DbError::Message(
3504 "Serial database contains more than one conflict branch".to_string(),
3505 ))
3506 }
3507 }
3508 }
3509 let Some(conflict) = conflict else {
3510 return Ok(None);
3511 };
3512 let conflict = *conflict;
3513 let expected_base = StoreWriteBase::Serial {
3514 branch_id: conflict.branch_id.clone(),
3515 base: conflict_exact_base.expect("conflict row carries exact base"),
3516 };
3517 let mut writes = Vec::new();
3518 for (write, base) in records {
3519 if matches!(base, StoreWriteBase::MergeConcurrent { .. }) {
3520 continue;
3521 }
3522 if base != expected_base {
3523 return Err(DbError::Message(
3524 "Serial database contains more than one unresolved branch".to_string(),
3525 ));
3526 }
3527 if !matches!(
3528 &write.status,
3529 WriteStatus::Conflict(_) | WriteStatus::Pending
3530 ) {
3531 return Err(DbError::Message(format!(
3532 "conflicted Serial branch write {} has non-resolvable status {:?}",
3533 write.write_id, write.status
3534 )));
3535 }
3536 writes.push(write);
3537 }
3538 Ok(Some(PendingBranch {
3539 branch_id: conflict.branch_id,
3540 base: conflict.base,
3541 current: conflict.current,
3542 writes,
3543 }))
3544 })
3545 .await
3546 }
3547
3548 pub async fn serial_branch_discard_state(
3549 &self,
3550 branch_id: &PendingBranchId,
3551 ) -> Result<SerialBranchDiscardState, DbError> {
3552 let branch_id = branch_id.clone();
3553 self.call(move |conn| {
3554 let abandonment: Option<String> = conn
3555 .query_row(
3556 "SELECT value FROM protocol_state WHERE key = ?1",
3557 [SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY],
3558 |row| row.get(0),
3559 )
3560 .optional()
3561 .map_err(DbError::from)?;
3562 if let Some(abandonment) = abandonment {
3563 let abandonment: DurableSerialCandidateAbandonment =
3564 serde_json::from_str(&abandonment).map_err(|error| {
3565 DbError::Message(format!("Serial candidate abandonment: {error}"))
3566 })?;
3567 if abandonment.branch_id != branch_id {
3568 return Err(DbError::Message(
3569 "another Serial branch owns candidate abandonment".to_string(),
3570 ));
3571 }
3572 return Ok(SerialBranchDiscardState::Abandonment);
3573 }
3574 let mut statement = conn
3575 .prepare(
3576 "SELECT base, status, prepared FROM store_writes
3577 WHERE status != '\"local_only\"'
3578 AND json_extract(status, '$.published') IS NULL
3579 AND json_extract(status, '$.resolved') IS NULL
3580 AND json_type(base, '$.serial') IS NOT NULL
3581 ORDER BY ordinal",
3582 )
3583 .map_err(DbError::from)?;
3584 let rows = statement
3585 .query_map([], |row| {
3586 Ok((
3587 row.get::<_, String>(0)?,
3588 row.get::<_, String>(1)?,
3589 row.get::<_, Option<String>>(2)?,
3590 ))
3591 })
3592 .map_err(DbError::from)?
3593 .collect::<Result<Vec<_>, _>>()
3594 .map_err(DbError::from)?;
3595 drop(statement);
3596 if rows.is_empty() {
3597 return Err(DbError::Message("Serial branch is absent".to_string()));
3598 }
3599 let mut base = None;
3600 let mut saw_local = false;
3601 let mut saw_prepared = false;
3602 let mut saw_conflict = false;
3603 for (raw_base, raw_status, raw_prepared) in rows {
3604 let StoreWriteBase::Serial {
3605 branch_id: stored_branch_id,
3606 base: stored_base,
3607 } = serde_json::from_str(&raw_base).map_err(|error| {
3608 DbError::Message(format!("Serial discard branch base: {error}"))
3609 })?
3610 else {
3611 unreachable!("selected Serial base")
3612 };
3613 if stored_branch_id != branch_id {
3614 return Err(DbError::Message(
3615 "Serial database contains another unresolved branch".to_string(),
3616 ));
3617 }
3618 if base.as_ref().is_some_and(|base| base != &stored_base) {
3619 return Err(DbError::Message(
3620 "Serial discard branch has inconsistent bases".to_string(),
3621 ));
3622 }
3623 base.get_or_insert(stored_base);
3624 let status: WriteStatus = serde_json::from_str(&raw_status)
3625 .map_err(|error| DbError::Message(format!("Serial discard status: {error}")))?;
3626 match status {
3627 WriteStatus::Pending | WriteStatus::Blocked(_) if raw_prepared.is_none() => {
3628 saw_local = true;
3629 }
3630 WriteStatus::Publishing if raw_prepared.is_some() => saw_prepared = true,
3631 WriteStatus::Conflict(conflict) if conflict.branch_id == branch_id => {
3632 saw_conflict = true;
3633 }
3634 other => {
3635 return Err(DbError::Message(format!(
3636 "Serial branch has non-discardable state {other:?}"
3637 )));
3638 }
3639 }
3640 }
3641 match (saw_local, saw_prepared, saw_conflict) {
3642 (true, false, false) => Ok(SerialBranchDiscardState::Local),
3643 (false, true, false) => Ok(SerialBranchDiscardState::Abandonment),
3644 (false, false, true) => {
3645 base.expect("nonempty branch");
3646 Ok(SerialBranchDiscardState::Conflict)
3647 }
3648 _ => Err(DbError::Message(
3649 "Serial branch mixes incompatible discard states".to_string(),
3650 )),
3651 }
3652 })
3653 .await
3654 }
3655
3656 pub async fn discard_local_serial_branch(
3657 &self,
3658 branch_id: PendingBranchId,
3659 ) -> Result<Vec<WriteId>, DbError> {
3660 let synced_tables = self.synced_tables().to_vec();
3661 let statuses = self.state.write_statuses.clone();
3662 self.call(move |conn| {
3663 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
3664 let mut statement = tx
3665 .prepare(
3666 "SELECT write_id, base, status, prepared, inverse_changeset
3667 FROM store_writes
3668 WHERE status != '\"local_only\"'
3669 AND json_extract(status, '$.published') IS NULL
3670 AND json_extract(status, '$.resolved') IS NULL
3671 AND json_type(base, '$.serial') IS NOT NULL
3672 ORDER BY ordinal",
3673 )
3674 .map_err(DbError::from)?;
3675 let rows = statement
3676 .query_map([], |row| {
3677 Ok((
3678 row.get::<_, String>(0)?,
3679 row.get::<_, String>(1)?,
3680 row.get::<_, String>(2)?,
3681 row.get::<_, Option<String>>(3)?,
3682 row.get::<_, Vec<u8>>(4)?,
3683 ))
3684 })
3685 .map_err(DbError::from)?
3686 .collect::<Result<Vec<_>, _>>()
3687 .map_err(DbError::from)?;
3688 drop(statement);
3689 if rows.is_empty() {
3690 return Err(DbError::Message("Serial branch is absent".to_string()));
3691 }
3692 let mut branch = Vec::new();
3693 for (write_id, raw_base, raw_status, prepared, inverse) in rows {
3694 let StoreWriteBase::Serial {
3695 branch_id: stored_branch_id,
3696 ..
3697 } = serde_json::from_str(&raw_base).map_err(|error| {
3698 DbError::Message(format!("local Serial discard base: {error}"))
3699 })?
3700 else {
3701 unreachable!("selected Serial base")
3702 };
3703 let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
3704 DbError::Message(format!("local Serial discard status: {error}"))
3705 })?;
3706 if stored_branch_id != branch_id
3707 || !matches!(status, WriteStatus::Pending | WriteStatus::Blocked(_))
3708 || prepared.is_some()
3709 {
3710 return Err(DbError::Message(
3711 "Serial branch is not locally discardable".to_string(),
3712 ));
3713 }
3714 branch.push((WriteId::from_generated(write_id), inverse));
3715 }
3716 let schema = Arc::new(crate::sync::conflict::TableSchema::from_db(
3717 &tx,
3718 &synced_tables,
3719 )?);
3720 for (_, inverse) in branch.iter().rev() {
3721 let inverse = crate::sync::apply::ValidatedChangeset::new(inverse, schema.clone())
3722 .map_err(|error| {
3723 DbError::Message(format!("invalid Serial inverse: {error}"))
3724 })?;
3725 crate::sync::apply::apply_changeset_strict_on(&tx, inverse)
3726 .map_err(|error| DbError::Message(format!("reverse Serial branch: {error}")))?;
3727 }
3728 let write_ids = branch
3729 .into_iter()
3730 .map(|(write_id, _)| write_id)
3731 .collect::<Vec<_>>();
3732 let resolution = WriteResolution::Discarded;
3733 Self::resolve_unpublished_writes_on(&tx, &write_ids, &resolution)?;
3734 tx.commit().map_err(DbError::from)?;
3735 let status = WriteStatus::Resolved(resolution);
3736 for write_id in &write_ids {
3737 Self::notify_write_status_in(&statuses, write_id, status.clone());
3738 }
3739 Ok(write_ids)
3740 })
3741 .await
3742 }
3743
3744 pub async fn conflicted_serial_branch_base(
3745 &self,
3746 branch_id: &PendingBranchId,
3747 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
3748 let branch = self.unresolved_serial_branch().await?.ok_or_else(|| {
3749 DbError::Message(format!(
3750 "Serial branch {} does not exist",
3751 branch_id.first_write_id()
3752 ))
3753 })?;
3754 if &branch.branch_id != branch_id || !branch.conflicted {
3755 return Err(DbError::Message(format!(
3756 "Serial branch {} is not conflicted",
3757 branch_id.first_write_id()
3758 )));
3759 }
3760 Ok(branch.base)
3761 }
3762
3763 pub(crate) async fn unresolved_serial_branch(
3764 &self,
3765 ) -> Result<Option<UnresolvedSerialBranch>, DbError> {
3766 self.call(|conn| {
3767 let mut statement = conn
3768 .prepare(
3769 "SELECT base, status FROM store_writes
3770 WHERE status != '\"local_only\"'
3771 AND json_extract(status, '$.published') IS NULL
3772 AND json_extract(status, '$.resolved') IS NULL
3773 AND json_type(base, '$.serial') IS NOT NULL
3774 ORDER BY ordinal",
3775 )
3776 .map_err(DbError::from)?;
3777 let rows = statement
3778 .query_map([], |row| {
3779 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
3780 })
3781 .map_err(DbError::from)?;
3782 let mut branch = None;
3783 for row in rows {
3784 let (raw_base, raw_status) = row.map_err(DbError::from)?;
3785 let StoreWriteBase::Serial { branch_id, base } = serde_json::from_str(&raw_base)
3786 .map_err(|error| {
3787 DbError::Message(format!("unresolved Serial base: {error}"))
3788 })?
3789 else {
3790 return Err(DbError::Message(
3791 "MergeConcurrent base reached a Serial branch query".to_string(),
3792 ));
3793 };
3794 let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
3795 DbError::Message(format!("unresolved Serial status: {error}"))
3796 })?;
3797 match &mut branch {
3798 None => {
3799 branch = Some(UnresolvedSerialBranch {
3800 branch_id,
3801 base,
3802 conflicted: matches!(status, WriteStatus::Conflict(_)),
3803 });
3804 }
3805 Some(existing) if existing.branch_id == branch_id && existing.base == base => {
3806 existing.conflicted |= matches!(status, WriteStatus::Conflict(_));
3807 }
3808 Some(_) => {
3809 return Err(DbError::Message(
3810 "Serial database contains more than one unresolved branch".to_string(),
3811 ));
3812 }
3813 }
3814 }
3815 Ok(branch)
3816 })
3817 .await
3818 }
3819
3820 pub async fn subscribe_write_status(
3821 &self,
3822 write_id: &WriteId,
3823 ) -> Result<tokio::sync::watch::Receiver<WriteStatus>, DbError> {
3824 let write_id = write_id.clone();
3825 let statuses = self.state.write_statuses.clone();
3826 self.call(move |conn| {
3827 let raw: String = conn
3828 .query_row(
3829 "SELECT status FROM store_writes WHERE write_id = ?1",
3830 [write_id.as_str()],
3831 |row| row.get(0),
3832 )
3833 .map_err(DbError::from)?;
3834 let current: WriteStatus = serde_json::from_str(&raw)
3835 .map_err(|error| DbError::Message(format!("write {write_id} status: {error}")))?;
3836 let mut senders = statuses.lock().expect("write status mutex poisoned");
3837 let sender = senders
3838 .entry(write_id)
3839 .or_insert_with(|| tokio::sync::watch::channel(current.clone()).0);
3840 sender.send_replace(current);
3841 Ok(sender.subscribe())
3842 })
3843 .await
3844 }
3845
3846 pub(crate) async fn lock_membership_load(&self) -> tokio::sync::OwnedMutexGuard<()> {
3849 self.state.membership_load.clone().lock_owned().await
3850 }
3851
3852 pub(crate) async fn lock_membership_mutation(&self) -> tokio::sync::OwnedMutexGuard<()> {
3853 self.state.membership_mutation.clone().lock_owned().await
3854 }
3855
3856 pub(crate) async fn lock_snapshot_publication(&self) -> tokio::sync::OwnedMutexGuard<()> {
3857 self.state.snapshot_publication.clone().lock_owned().await
3858 }
3859
3860 pub(crate) async fn lock_local_blob_cleanup(&self) -> tokio::sync::OwnedMutexGuard<()> {
3861 self.state.local_blob_cleanup.clone().lock_owned().await
3862 }
3863
3864 #[cfg(any(test, feature = "test-utils"))]
3865 #[doc(hidden)]
3866 pub fn arm_test_pause(
3867 &self,
3868 point: DatabaseTestPoint,
3869 ) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
3870 self.state.test_pause_points.arm(point)
3871 }
3872
3873 #[cfg(any(test, feature = "test-utils"))]
3874 #[doc(hidden)]
3875 pub fn observe_test_points(&self) -> tokio::sync::mpsc::UnboundedReceiver<DatabaseTestPoint> {
3876 self.state.test_pause_points.observe()
3877 }
3878
3879 #[cfg(any(test, feature = "test-utils"))]
3880 pub(crate) async fn reach_test_point(&self, point: DatabaseTestPoint) {
3881 self.state.test_pause_points.reach(point).await;
3882 }
3883
3884 pub fn open(
3894 path: &Path,
3895 synced_tables: Vec<SyncedTable>,
3896 blob_tombstone_grace: chrono::Duration,
3897 transfer_limits: crate::blob::TransferLimits,
3898 write_policy: WritePolicy,
3899 device_id: String,
3900 migrations: &[Migration],
3901 ) -> Result<(Database, UpdatedAtStamper), OpenError> {
3902 let hlc =
3903 Hlc::try_new(device_id).map_err(|e| DbError::Message(format!("device_id {e}")))?;
3904 Self::open_with_hlc_and_coven_metadata(
3905 path,
3906 synced_tables,
3907 blob_tombstone_grace,
3908 transfer_limits,
3909 write_policy,
3910 Arc::new(hlc),
3911 migrations,
3912 CovenMetadataOpen::Detect,
3913 )
3914 }
3915
3916 pub(crate) fn open_initialized_store(
3917 path: &Path,
3918 synced_tables: Vec<SyncedTable>,
3919 blob_tombstone_grace: chrono::Duration,
3920 transfer_limits: crate::blob::TransferLimits,
3921 write_policy: WritePolicy,
3922 device_id: String,
3923 migrations: &[Migration],
3924 ) -> Result<(Database, UpdatedAtStamper), OpenError> {
3925 let hlc =
3926 Hlc::try_new(device_id).map_err(|e| DbError::Message(format!("device_id {e}")))?;
3927 Self::open_with_hlc_and_coven_metadata(
3928 path,
3929 synced_tables,
3930 blob_tombstone_grace,
3931 transfer_limits,
3932 write_policy,
3933 Arc::new(hlc),
3934 migrations,
3935 CovenMetadataOpen::InitializeVerifiedSnapshot,
3936 )
3937 }
3938
3939 #[cfg(any(test, feature = "test-utils"))]
3946 pub(crate) fn open_with_hlc(
3947 path: &Path,
3948 synced_tables: Vec<SyncedTable>,
3949 blob_tombstone_grace: chrono::Duration,
3950 transfer_limits: crate::blob::TransferLimits,
3951 write_policy: WritePolicy,
3952 hlc: Arc<Hlc>,
3953 migrations: &[Migration],
3954 ) -> Result<(Database, UpdatedAtStamper), OpenError> {
3955 Self::open_with_hlc_and_coven_metadata(
3956 path,
3957 synced_tables,
3958 blob_tombstone_grace,
3959 transfer_limits,
3960 write_policy,
3961 hlc,
3962 migrations,
3963 CovenMetadataOpen::Detect,
3964 )
3965 }
3966
3967 fn open_with_hlc_and_coven_metadata(
3968 path: &Path,
3969 synced_tables: Vec<SyncedTable>,
3970 blob_tombstone_grace: chrono::Duration,
3971 transfer_limits: crate::blob::TransferLimits,
3972 write_policy: WritePolicy,
3973 hlc: Arc<Hlc>,
3974 migrations: &[Migration],
3975 metadata_open: CovenMetadataOpen,
3976 ) -> Result<(Database, UpdatedAtStamper), OpenError> {
3977 let (core, state, stamper) = DatabaseCore::open(
3978 path,
3979 synced_tables,
3980 blob_tombstone_grace,
3981 transfer_limits,
3982 write_policy,
3983 hlc,
3984 migrations,
3985 metadata_open,
3986 )?;
3987
3988 let database = {
3989 let (jobs_tx, jobs_rx) = tokio::sync::mpsc::unbounded_channel::<DbJob>();
3990 let join = std::thread::Builder::new()
3991 .name("coven-db".to_string())
3992 .spawn(move || run_connection_thread(core, jobs_rx))
3993 .map_err(|e| DbError::Message(format!("spawn database connection thread: {e}")))?;
3994 Database {
3995 thread: Arc::new(ConnectionThread {
3996 jobs: jobs_tx,
3997 join: Some(join),
3998 }),
3999 state,
4000 }
4001 };
4002
4003 Ok((database, stamper))
4004 }
4005
4006 pub fn open_read_only(
4022 path: &Path,
4023 synced_tables: Vec<SyncedTable>,
4024 blob_tombstone_grace: chrono::Duration,
4025 transfer_limits: crate::blob::TransferLimits,
4026 write_policy: WritePolicy,
4027 device_id: String,
4028 migrations: &[Migration],
4029 ) -> Result<Database, OpenError> {
4030 let hlc =
4031 Hlc::try_new(device_id).map_err(|e| DbError::Message(format!("device_id {e}")))?;
4032 let (core, state) = DatabaseCore::open_read_only(
4033 path,
4034 synced_tables,
4035 blob_tombstone_grace,
4036 transfer_limits,
4037 write_policy,
4038 Arc::new(hlc),
4039 migrations,
4040 )?;
4041 let (jobs_tx, jobs_rx) = tokio::sync::mpsc::unbounded_channel::<DbJob>();
4042 let join = std::thread::Builder::new()
4043 .name("coven-db-ro".to_string())
4044 .spawn(move || run_connection_thread(core, jobs_rx))
4045 .map_err(|e| DbError::Message(format!("spawn database connection thread: {e}")))?;
4046 Ok(Database {
4047 thread: Arc::new(ConnectionThread {
4048 jobs: jobs_tx,
4049 join: Some(join),
4050 }),
4051 state,
4052 })
4053 }
4054
4055 pub fn synced_tables(&self) -> &[SyncedTable] {
4061 &self.state.synced_tables
4062 }
4063
4064 pub fn blob_tombstone_grace(&self) -> chrono::Duration {
4068 self.state.blob_tombstone_grace
4069 }
4070
4071 pub fn transfer_limits(&self) -> crate::blob::TransferLimits {
4075 self.state.transfer_limits
4076 }
4077
4078 pub fn write_policy(&self) -> WritePolicy {
4079 self.state.write_policy
4080 }
4081
4082 #[doc(hidden)]
4085 pub fn gates(&self) -> Arc<Gates> {
4086 self.state.gates.clone()
4087 }
4088
4089 #[doc(hidden)]
4092 pub fn blob_decls(&self) -> Arc<BlobDecls> {
4093 self.state.blob_decls.clone()
4094 }
4095
4096 pub fn schema_version(&self) -> u32 {
4104 self.state.schema_version
4105 }
4106
4107 pub fn sync_routing_hash(&self) -> ObjectHash {
4110 self.state.sync_routing_hash
4111 }
4112
4113 pub fn hlc(&self) -> Arc<Hlc> {
4116 self.state.hlc.clone()
4117 }
4118
4119 pub fn stamper(&self) -> UpdatedAtStamper {
4120 UpdatedAtStamper::new(self.state.hlc.clone())
4121 }
4122
4123 pub(crate) fn receive_wall_ms(&self) -> u64 {
4128 self.state.hlc.wall_now_ms()
4129 }
4130
4131 pub async fn call<F, R>(&self, f: F) -> Result<R, DbError>
4142 where
4143 F: FnOnce(&Connection) -> Result<R, DbError> + Send + 'static,
4144 R: Send + 'static,
4145 {
4146 self.on_connection_thread(move |core| f(core.connection()))
4147 .await
4148 }
4149
4150 async fn on_connection_thread<F, R>(&self, f: F) -> R
4166 where
4167 F: FnOnce(&mut DatabaseCore) -> R + Send + 'static,
4168 R: Send + 'static,
4169 {
4170 let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
4171 let job = DbJob::Run(Box::new(move |core| {
4172 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(core)));
4173 let _ = reply_tx.send(outcome);
4176 }));
4177 if self.thread.jobs.send(job).is_err() {
4178 panic!("database connection thread stopped before a call completed");
4179 }
4180 match reply_rx.await {
4181 Ok(Ok(value)) => value,
4182 Ok(Err(panic)) => std::panic::resume_unwind(panic),
4183 Err(_) => {
4184 panic!("database connection thread dropped a call's reply without responding")
4185 }
4186 }
4187 }
4188
4189 fn start_host_change_journal_on<'c>(
4190 conn: &'c Connection,
4191 synced_tables: &[SyncedTable],
4192 ) -> Result<rusqlite::session::Session<'c>, DbError> {
4193 attach_session(conn, synced_tables)
4194 }
4195
4196 fn drain_host_change_journal_on(
4197 session: &mut rusqlite::session::Session<'_>,
4198 ) -> Result<Vec<u8>, DbError> {
4199 capture_changeset(session)
4200 }
4201
4202 fn invert_changeset(changeset: &[u8]) -> Result<Vec<u8>, DbError> {
4203 if changeset.is_empty() {
4204 return Ok(Vec::new());
4205 }
4206 let mut inverse = Vec::new();
4207 rusqlite::session::invert_strm(&mut &changeset[..], &mut inverse).map_err(DbError::from)?;
4208 Ok(inverse)
4209 }
4210
4211 fn run_host_sql_on<R, E>(conn: &Connection, f: impl FnOnce() -> Result<R, E>) -> Result<R, E>
4212 where
4213 E: From<DbError>,
4214 {
4215 conn.authorizer(Some(authorize_host_sql))
4216 .map_err(DbError::from)
4217 .map_err(E::from)?;
4218 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
4219 if let Err(error) = conn.authorizer(
4220 None::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>,
4221 ) {
4222 panic!("failed to remove host SQL gate-baseline guard: {error}");
4223 }
4224 match outcome {
4225 Ok(result) => result,
4226 Err(panic) => std::panic::resume_unwind(panic),
4227 }
4228 }
4229
4230 fn capture_host_changes_on<R, E>(
4231 conn: &Connection,
4232 synced_tables: &[SyncedTable],
4233 f: impl FnOnce() -> Result<R, E>,
4234 ) -> Result<(R, Vec<u8>), E>
4235 where
4236 E: From<DbError>,
4237 {
4238 let mut journal =
4239 Self::start_host_change_journal_on(conn, synced_tables).map_err(E::from)?;
4240 let value = Self::run_host_sql_on(conn, f)?;
4241 let captured = Self::drain_host_change_journal_on(&mut journal).map_err(E::from)?;
4242 crate::sync::session::validate_changeset_row_identities(&captured, synced_tables)
4243 .map_err(|error| DbError::Message(error.to_string()))
4244 .map_err(E::from)?;
4245 Ok((value, captured))
4246 }
4247
4248 fn capture_store_write_blob_facts_on(
4249 tx: &rusqlite::Transaction<'_>,
4250 changeset: &[u8],
4251 blob_decls: &BlobDecls,
4252 ) -> Result<StoreWriteBlobFacts, DbError> {
4253 let changes = crate::changeset::walk(changeset)
4254 .map_err(|error| DbError::Message(format!("read Store write blobs: {error}")))?;
4255 let mut facts = BTreeMap::new();
4256 for change in changes {
4257 let Some(publication) = blob_decls
4258 .publication_blob_from_change(tx, &change)
4259 .map_err(|error| DbError::Message(format!("capture Store write blob: {error}")))?
4260 else {
4261 continue;
4262 };
4263 let plaintext_hash = publication.plaintext_hash.parse().map_err(|error| {
4264 DbError::Message(format!(
4265 "capture Store write blob {}/{} plaintext hash: {error}",
4266 publication.blob.namespace, publication.blob.id
4267 ))
4268 })?;
4269 let external_path = if publication.blob.provenance == Provenance::UserProvided {
4270 tx.query_row(
4271 "SELECT path FROM local_blob_refs
4272 WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3
4273 AND row_stamp = ?4 AND namespace = ?5 AND blob_id = ?6",
4274 rusqlite::params![
4275 publication.table,
4276 publication.row_id,
4277 publication.column,
4278 publication.row_stamp,
4279 publication.blob.namespace,
4280 publication.blob.id,
4281 ],
4282 |row| row.get::<_, String>(0),
4283 )
4284 .optional()
4285 .map_err(DbError::from)?
4286 .map(PathBuf::from)
4287 } else {
4288 None
4289 };
4290 let previous = previous_row_blob_for_write_on(
4291 tx,
4292 &publication.table,
4293 &publication.row_id,
4294 &publication.row_stamp,
4295 &publication.column,
4296 &publication.blob,
4297 publication.plaintext_size,
4298 plaintext_hash,
4299 )?;
4300 let fact = StoreWriteBlobFact {
4301 table: publication.table,
4302 row_id: publication.row_id,
4303 row_stamp: publication.row_stamp,
4304 column: publication.column,
4305 blob: publication.blob,
4306 plaintext_size: publication.plaintext_size,
4307 plaintext_hash,
4308 external_path,
4309 previous,
4310 };
4311 let key = fact.identity_key();
4312 if let Some(prior) = facts.insert(key.clone(), fact.clone()) {
4313 if prior != fact {
4314 return Err(DbError::Message(format!(
4315 "Store write gives row {}/{}/{} at {} conflicting blob facts",
4316 key.0, key.1, key.2, key.3
4317 )));
4318 }
4319 }
4320 }
4321 Ok(StoreWriteBlobFacts {
4322 blobs: facts.into_values().collect(),
4323 })
4324 }
4325
4326 fn capture_partition_blob_facts_on(
4327 tx: &rusqlite::Transaction<'_>,
4328 partitions: &[gate::AudiencePartition],
4329 blob_decls: &BlobDecls,
4330 ) -> Result<StoreWriteBlobFacts, DbError> {
4331 let mut facts = BTreeMap::new();
4332 for partition in partitions {
4333 for fact in
4334 Self::capture_store_write_blob_facts_on(tx, &partition.changeset, blob_decls)?.blobs
4335 {
4336 let key = fact.identity_key();
4337 if let Some(prior) = facts.insert(key.clone(), fact.clone()) {
4338 if prior != fact {
4339 return Err(DbError::Message(format!(
4340 "audience partitions give row {}/{}/{} at {} conflicting blob facts",
4341 key.0, key.1, key.2, key.3
4342 )));
4343 }
4344 }
4345 }
4346 }
4347 Ok(StoreWriteBlobFacts {
4348 blobs: facts.into_values().collect(),
4349 })
4350 }
4351
4352 fn partition_captured_write_on(
4353 tx: &rusqlite::Transaction<'_>,
4354 captured: &[u8],
4355 gates: &Gates,
4356 write_policy: WritePolicy,
4357 routing: StoreWriteRouting<'_>,
4358 ) -> Result<Vec<gate::AudiencePartition>, DbError> {
4359 match routing {
4360 StoreWriteRouting::MergeScoped(encryption) => {
4361 let store_root_hash = Self::required_store_root_hash_on(tx)?;
4362 let key = crate::sync::circle::derive_row_routing_key(encryption, store_root_hash)
4363 .map_err(|error| {
4364 DbError::Message(format!("derive row routing key: {error}"))
4365 })?;
4366 let routing_changeset = gate::capture_routing_changes(tx, captured, gates, &key)
4367 .map_err(|error| {
4368 DbError::Message(format!("capture scoped routing changes: {error}"))
4369 })?;
4370 gate::partition_outbound(tx, captured, &routing_changeset, gates, write_policy)
4371 .map_err(|error| {
4372 DbError::Message(format!("partition scoped host transaction: {error}"))
4373 })
4374 }
4375 StoreWriteRouting::SerialScoped => gate::partition_outbound(
4376 tx,
4377 captured,
4378 &gate::RoutingChanges::empty(),
4379 gates,
4380 write_policy,
4381 )
4382 .map_err(|error| {
4383 DbError::Message(format!("partition scoped host transaction: {error}"))
4384 }),
4385 StoreWriteRouting::Unscoped => {
4386 let changeset = gate::gate_outbound(tx, captured, gates)
4387 .map_err(|error| DbError::Message(format!("gate host transaction: {error}")))?;
4388 Ok((!changeset.is_empty())
4389 .then_some(gate::AudiencePartition {
4390 audience: crate::sync::circle::Audience::Store,
4391 control: None,
4392 changeset,
4393 })
4394 .into_iter()
4395 .collect())
4396 }
4397 }
4398 }
4399
4400 fn store_write_routing<'a>(
4401 gates: &Gates,
4402 write_policy: WritePolicy,
4403 routing_encryption: Option<&'a EncryptionService>,
4404 ) -> Result<StoreWriteRouting<'a>, DbError> {
4405 if !gates.has_scoped_graph() {
4406 return Ok(StoreWriteRouting::Unscoped);
4407 }
4408 match write_policy {
4409 WritePolicy::MergeConcurrent => routing_encryption
4410 .map(StoreWriteRouting::MergeScoped)
4411 .ok_or_else(|| {
4412 DbError::Message(
4413 "Merge scoped write requires the Store generation-1 routing key"
4414 .to_string(),
4415 )
4416 }),
4417 WritePolicy::Serial => Ok(StoreWriteRouting::SerialScoped),
4418 }
4419 }
4420
4421 pub(crate) fn validate_store_write_routing(
4422 gates: &Gates,
4423 write_policy: WritePolicy,
4424 routing_encryption: Option<&EncryptionService>,
4425 ) -> Result<(), DbError> {
4426 Self::store_write_routing(gates, write_policy, routing_encryption).map(drop)
4427 }
4428
4429 fn insert_store_write_on(
4430 tx: &rusqlite::Transaction<'_>,
4431 write_id: &WriteId,
4432 partitions: &[gate::AudiencePartition],
4433 inverse_changeset: &[u8],
4434 base: &StoreWriteBase,
4435 blob_facts: &StoreWriteBlobFacts,
4436 rows_changed: u64,
4437 ) -> Result<WriteStatus, DbError> {
4438 let affected_rows = if partitions.is_empty() {
4439 if rows_changed == 0 {
4450 warn!("journaled sql transaction changed nothing; pure reads belong on sql_read");
4451 #[cfg(debug_assertions)]
4455 warn!(
4456 "zero-change sql transaction backtrace:\n{}",
4457 std::backtrace::Backtrace::force_capture()
4458 );
4459 }
4460 Vec::new()
4461 } else {
4462 let mut affected = Vec::new();
4463 for partition in partitions {
4464 affected.extend(
4465 crate::changeset::walk(&partition.changeset)
4466 .map_err(|error| {
4467 DbError::Message(format!("read affected write rows: {error}"))
4468 })?
4469 .into_iter()
4470 .filter(|row| !gate::is_routing_table(&row.table))
4471 .map(|row| {
4472 let primary_key = row.pk().map(str::to_owned).ok_or_else(|| {
4473 DbError::Message(format!(
4474 "shared write row in {:?} has no primary key",
4475 row.table
4476 ))
4477 })?;
4478 Ok(AffectedRow {
4479 table: row.table,
4480 primary_key,
4481 })
4482 })
4483 .collect::<Result<Vec<_>, DbError>>()?,
4484 );
4485 }
4486 affected.sort();
4487 affected.dedup();
4488 affected
4489 };
4490 let status = if partitions.is_empty() {
4491 WriteStatus::LocalOnly
4492 } else {
4493 WriteStatus::Pending
4494 };
4495 let base = serde_json::to_string(base)
4496 .map_err(|error| DbError::Message(format!("serialize pending Store base: {error}")))?;
4497 let status_json = serde_json::to_string(&status)
4498 .map_err(|error| DbError::Message(format!("serialize write status: {error}")))?;
4499 let affected_rows = serde_json::to_string(&affected_rows)
4500 .map_err(|error| DbError::Message(format!("serialize affected rows: {error}")))?;
4501 let blob_facts_json = serde_json::to_string(blob_facts).map_err(|error| {
4502 DbError::Message(format!("serialize Store write blob facts: {error}"))
4503 })?;
4504 let store_changeset = partitions
4505 .iter()
4506 .find(|partition| partition.audience == crate::sync::circle::Audience::Store)
4507 .map(|partition| partition.changeset.as_slice())
4508 .unwrap_or_default();
4509 tx.execute(
4510 "INSERT INTO store_writes
4511 (write_id, status, affected_rows, changeset, inverse_changeset, base, blob_facts)
4512 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
4513 rusqlite::params![
4514 write_id.as_str(),
4515 status_json,
4516 affected_rows,
4517 store_changeset,
4518 inverse_changeset,
4519 base,
4520 blob_facts_json,
4521 ],
4522 )
4523 .map_err(DbError::from)?;
4524 for partition in partitions {
4525 let audience = match partition.audience {
4526 crate::sync::circle::Audience::Store => "store".to_string(),
4527 crate::sync::circle::Audience::Local => {
4528 return Err(DbError::Message(
4529 "Local audience must not enter the durable outbound journal".to_string(),
4530 ));
4531 }
4532 crate::sync::circle::Audience::Circle(circle_id) => circle_id.to_string(),
4533 };
4534 let control = partition
4535 .control
4536 .as_ref()
4537 .map(gate::CirclePartitionControl::stored_json);
4538 tx.execute(
4539 "INSERT INTO store_write_partitions
4540 (write_id, audience, control_coord, changeset)
4541 VALUES (?1, ?2, ?3, ?4)",
4542 rusqlite::params![write_id.as_str(), audience, control, partition.changeset,],
4543 )
4544 .map_err(DbError::from)?;
4545 }
4546 if status == WriteStatus::Pending {
4547 for fact in &blob_facts.blobs {
4548 if fact.blob.provenance != Provenance::HostProvided {
4549 continue;
4550 }
4551 tx.execute(
4552 "INSERT OR IGNORE INTO store_write_blob_leases
4553 (write_id, namespace, blob_id) VALUES (?1, ?2, ?3)",
4554 (write_id.as_str(), &fact.blob.namespace, &fact.blob.id),
4555 )
4556 .map_err(DbError::from)?;
4557 }
4558 }
4559 Ok(status)
4560 }
4561
4562 pub fn run_store_write_transaction_on<R, E>(
4563 conn: &Connection,
4564 synced_tables: &[SyncedTable],
4565 gates: &Gates,
4566 blob_decls: &BlobDecls,
4567 write_policy: WritePolicy,
4568 routing_encryption: Option<&EncryptionService>,
4569 write_id: WriteId,
4570 f: impl FnOnce(&rusqlite::Transaction<'_>) -> Result<R, E>,
4571 ) -> Result<WriteReceipt<R>, E>
4572 where
4573 E: From<DbError>,
4574 {
4575 (|| {
4576 let routing = Self::store_write_routing(gates, write_policy, routing_encryption)
4577 .map_err(E::from)?;
4578 let changes_before = conn.total_changes();
4579 let tx = conn
4580 .unchecked_transaction()
4581 .map_err(DbError::from)
4582 .map_err(E::from)?;
4583 let (value, captured) = Self::capture_host_changes_on(&tx, synced_tables, || f(&tx))?;
4584 let partitions =
4585 Self::partition_captured_write_on(&tx, &captured, gates, write_policy, routing)
4586 .map_err(E::from)?;
4587 let blob_facts = Self::capture_partition_blob_facts_on(&tx, &partitions, blob_decls)
4588 .map_err(E::from)?;
4589 let rows_changed = conn.total_changes().saturating_sub(changes_before);
4590 let local_stream_id = local_merge_stream_id_on(&tx).map_err(E::from)?;
4591 let inverse_changeset = Self::invert_changeset(&captured).map_err(E::from)?;
4592 let base = match write_policy {
4593 WritePolicy::MergeConcurrent => StoreWriteBase::MergeConcurrent {
4594 dependencies: Self::materialized_frontier_on(&tx, local_stream_id.as_deref())
4595 .map_err(E::from)?,
4596 },
4597 WritePolicy::Serial => {
4598 let existing: Option<String> = tx
4599 .query_row(
4600 "SELECT base FROM store_writes
4601 WHERE status != '\"local_only\"'
4602 AND json_extract(status, '$.published') IS NULL
4603 AND json_extract(status, '$.resolved') IS NULL
4604 AND json_type(base, '$.serial') IS NOT NULL
4605 ORDER BY ordinal LIMIT 1",
4606 [],
4607 |row| row.get(0),
4608 )
4609 .optional()
4610 .map_err(DbError::from)
4611 .map_err(E::from)?;
4612 match existing {
4613 Some(existing) => serde_json::from_str(&existing)
4614 .map_err(|error| {
4615 DbError::Message(format!("pending serial branch base: {error}"))
4616 })
4617 .map_err(E::from)?,
4618 None => StoreWriteBase::Serial {
4619 branch_id: PendingBranchId::from_first_write(write_id.clone()),
4620 base: Self::latest_position_for_device_on(&tx, SERIAL_STREAM_ID)
4621 .map_err(E::from)?,
4622 },
4623 }
4624 }
4625 };
4626 let status = Self::insert_store_write_on(
4627 &tx,
4628 &write_id,
4629 &partitions,
4630 &inverse_changeset,
4631 &base,
4632 &blob_facts,
4633 rows_changed,
4634 )
4635 .map_err(E::from)?;
4636 let pending_branch_id = match (&status, &base) {
4637 (WriteStatus::LocalOnly, _) | (_, StoreWriteBase::MergeConcurrent { .. }) => None,
4638 (_, StoreWriteBase::Serial { branch_id, .. }) => Some(branch_id.clone()),
4639 };
4640 tx.commit().map_err(DbError::from).map_err(E::from)?;
4641 Ok(WriteReceipt {
4642 value,
4643 write_id,
4644 status,
4645 pending_branch_id,
4646 })
4647 })()
4648 }
4649
4650 pub fn run_internal_store_write_transaction_on<R, E>(
4651 conn: &Connection,
4652 synced_tables: &[SyncedTable],
4653 write_policy: WritePolicy,
4654 routing_encryption: Option<&EncryptionService>,
4655 write_id: WriteId,
4656 f: impl FnOnce(&rusqlite::Transaction<'_>) -> Result<R, E>,
4657 ) -> Result<R, E>
4658 where
4659 E: From<DbError>,
4660 {
4661 let gates = Gates::from_tables(conn, synced_tables)
4662 .map_err(|error| DbError::Message(error.to_string()))
4663 .map_err(E::from)?;
4664 let blob_decls = BlobDecls::from_tables(conn, synced_tables)
4665 .map_err(|error| DbError::Message(error.to_string()))
4666 .map_err(E::from)?;
4667 Self::run_store_write_transaction_on(
4668 conn,
4669 synced_tables,
4670 &gates,
4671 &blob_decls,
4672 write_policy,
4673 routing_encryption,
4674 write_id,
4675 f,
4676 )
4677 .map(|receipt| receipt.value)
4678 }
4679
4680 pub(crate) async fn prepare_store_write(&self) -> Result<Option<PreparedStoreWrite>, DbError> {
4681 let write_policy = self.write_policy();
4682 self.call(move |conn| {
4683 let stored = conn
4684 .query_row(
4685 "SELECT write_id, changeset, inverse_changeset, base, blob_facts FROM store_writes
4686 WHERE status = '\"pending\"'
4687 AND ordinal = (
4688 SELECT MIN(ordinal) FROM store_writes
4689 WHERE status != '\"local_only\"'
4690 AND json_extract(status, '$.published') IS NULL
4691 AND json_extract(status, '$.resolved') IS NULL
4692 )
4693 AND NOT EXISTS (
4694 SELECT 1 FROM store_writes WHERE prepared IS NOT NULL
4695 )
4696 ORDER BY ordinal LIMIT 1",
4697 [],
4698 |row| {
4699 Ok((
4700 row.get::<_, String>(0)?,
4701 row.get::<_, Vec<u8>>(1)?,
4702 row.get::<_, Vec<u8>>(2)?,
4703 row.get::<_, String>(3)?,
4704 row.get::<_, String>(4)?,
4705 ))
4706 },
4707 )
4708 .optional()
4709 .map_err(DbError::from)?;
4710 let Some((write_id, changeset, inverse_changeset, base, blob_facts)) = stored else {
4711 return Ok(None);
4712 };
4713 let partitions = Self::prepared_store_write_partitions_on(
4714 conn,
4715 &write_id,
4716 &changeset,
4717 write_policy,
4718 )?;
4719 Ok(Some(PreparedStoreWrite {
4720 write_id: WriteId::from_generated(write_id),
4721 changeset,
4722 partitions,
4723 inverse_changeset,
4724 base: serde_json::from_str(&base)
4725 .map_err(|error| DbError::Message(format!("pending write base: {error}")))?,
4726 blob_facts: serde_json::from_str(&blob_facts).map_err(|error| {
4727 DbError::Message(format!("pending write blob facts: {error}"))
4728 })?,
4729 }))
4730 })
4731 .await
4732 }
4733
4734 fn prepared_store_write_partitions_on(
4735 conn: &Connection,
4736 write_id: &str,
4737 stored_store_changeset: &[u8],
4738 write_policy: WritePolicy,
4739 ) -> Result<PreparedStoreWritePartitions, DbError> {
4740 let mut statement = conn
4741 .prepare(
4742 "SELECT audience, control_coord, changeset
4743 FROM store_write_partitions
4744 WHERE write_id = ?1
4745 ORDER BY CASE audience WHEN 'store' THEN 0 ELSE 1 END,
4746 audience, control_coord",
4747 )
4748 .map_err(DbError::from)?;
4749 let rows = statement
4750 .query_map([write_id], |row| {
4751 Ok((
4752 row.get::<_, String>(0)?,
4753 row.get::<_, Option<String>>(1)?,
4754 row.get::<_, Vec<u8>>(2)?,
4755 ))
4756 })
4757 .map_err(DbError::from)?;
4758 let mut store = None;
4759 let mut circles = Vec::new();
4760 for row in rows {
4761 let (audience, control, changeset) = row.map_err(DbError::from)?;
4762 if audience == "store" {
4763 if control.is_some() {
4764 return Err(DbError::Message(format!(
4765 "pending write {write_id} Store partition carries a Circle control"
4766 )));
4767 }
4768 if store.is_some() {
4769 return Err(DbError::Message(format!(
4770 "pending write {write_id} carries more than one Store partition"
4771 )));
4772 }
4773 store = Some(gate::AudiencePartition {
4774 audience: crate::sync::circle::Audience::Store,
4775 control: None,
4776 changeset,
4777 });
4778 continue;
4779 }
4780 let circle_id = audience
4781 .parse::<crate::sync::circle::CircleId>()
4782 .map_err(|error| {
4783 DbError::Message(format!(
4784 "pending write {write_id} has invalid audience {audience:?}: {error}"
4785 ))
4786 })?;
4787 let control_json = control.ok_or_else(|| {
4788 DbError::Message(format!(
4789 "pending write {write_id} Circle {circle_id} has no control coordinate"
4790 ))
4791 })?;
4792 let control =
4793 gate::CirclePartitionControl::from_stored_json(control_json).map_err(|error| {
4794 DbError::Message(format!(
4795 "pending write {write_id} Circle {circle_id} control coordinate: {error}"
4796 ))
4797 })?;
4798 let control_policy = match control.coordinate() {
4799 crate::sync::circle::CircleControlCoord::MergeConcurrent { .. } => {
4800 WritePolicy::MergeConcurrent
4801 }
4802 crate::sync::circle::CircleControlCoord::Serial { .. } => WritePolicy::Serial,
4803 };
4804 if control_policy != write_policy {
4805 return Err(DbError::Message(format!(
4806 "pending write {write_id} Circle {circle_id} control uses {control_policy:?}, database uses {write_policy:?}"
4807 )));
4808 }
4809 circles.push(gate::AudiencePartition {
4810 audience: crate::sync::circle::Audience::Circle(circle_id),
4811 control: Some(control),
4812 changeset,
4813 });
4814 }
4815 drop(statement);
4816 match &store {
4817 Some(partition) if partition.changeset != stored_store_changeset => {
4818 return Err(DbError::Message(format!(
4819 "pending write {write_id} Store partition differs from store_writes.changeset"
4820 )));
4821 }
4822 None if !stored_store_changeset.is_empty() => {
4823 return Err(DbError::Message(format!(
4824 "pending write {write_id} has a store_writes.changeset without a Store partition"
4825 )));
4826 }
4827 Some(_) | None => {}
4828 }
4829 if store.is_none() && circles.is_empty() {
4830 return Err(DbError::Message(format!(
4831 "pending write {write_id} has no durable audience partitions"
4832 )));
4833 }
4834 Ok(PreparedStoreWritePartitions { store, circles })
4835 }
4836
4837 pub(crate) async fn reserve_serial_store_branch(
4838 &self,
4839 ) -> Result<Option<SerialStoreBranchPreparationWork>, DbError> {
4840 if self.write_policy() != WritePolicy::Serial {
4841 return Err(DbError::Message(
4842 "Serial branch reservation requires the Serial write policy".to_string(),
4843 ));
4844 }
4845 let statuses = self.state.write_statuses.clone();
4846 self.call(move |conn| {
4847 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
4848 let mut statement = tx
4849 .prepare(
4850 "SELECT write_id, changeset, inverse_changeset, base, blob_facts, status, prepared
4851 FROM store_writes
4852 WHERE status != '\"local_only\"'
4853 AND json_extract(status, '$.published') IS NULL
4854 AND json_extract(status, '$.resolved') IS NULL
4855 ORDER BY ordinal",
4856 )
4857 .map_err(DbError::from)?;
4858 let rows = statement
4859 .query_map([], |row| {
4860 Ok((
4861 row.get::<_, String>(0)?,
4862 row.get::<_, Vec<u8>>(1)?,
4863 row.get::<_, Vec<u8>>(2)?,
4864 row.get::<_, String>(3)?,
4865 row.get::<_, String>(4)?,
4866 row.get::<_, String>(5)?,
4867 row.get::<_, Option<String>>(6)?,
4868 ))
4869 })
4870 .map_err(DbError::from)?;
4871 let mut records = Vec::new();
4872 for row in rows {
4873 records.push(row.map_err(DbError::from)?);
4874 }
4875 drop(statement);
4876 let Some(first) = records.first() else {
4877 tx.commit().map_err(DbError::from)?;
4878 return Ok(None);
4879 };
4880 let first_base: StoreWriteBase = serde_json::from_str(&first.3)
4881 .map_err(|error| DbError::Message(format!("pending Serial write base: {error}")))?;
4882 let StoreWriteBase::Serial { branch_id, base } = first_base else {
4883 return Err(DbError::Message(
4884 "MergeConcurrent write exists in a Serial database".to_string(),
4885 ));
4886 };
4887 let preparing = serde_json::to_string(&PreparedStoreWriteState::SerialPreparing)
4888 .map_err(|error| DbError::Message(format!("serialize Serial reservation: {error}")))?;
4889 let publishing = serde_json::to_string(&WriteStatus::Publishing)
4890 .map_err(|error| DbError::Message(format!("serialize write status: {error}")))?;
4891 let mut writes = Vec::new();
4892 let mut newly_reserved = Vec::new();
4893 for (write_id, changeset, inverse_changeset, raw_base, blob_facts, status, prepared) in
4894 records
4895 {
4896 let parsed_base: StoreWriteBase = serde_json::from_str(&raw_base)
4897 .map_err(|error| DbError::Message(format!("pending Serial write base: {error}")))?;
4898 if parsed_base
4899 != (StoreWriteBase::Serial {
4900 branch_id: branch_id.clone(),
4901 base: base.clone(),
4902 })
4903 {
4904 break;
4905 }
4906 match (status.as_str(), prepared.as_deref()) {
4907 ("\"pending\"", None) => {
4908 let updated = tx
4909 .execute(
4910 "UPDATE store_writes SET status = ?2, prepared = ?3
4911 WHERE write_id = ?1 AND status = '\"pending\"' AND prepared IS NULL",
4912 rusqlite::params![&write_id, &publishing, &preparing],
4913 )
4914 .map_err(DbError::from)?;
4915 if updated != 1 {
4916 return Err(DbError::Message(format!(
4917 "Serial write {write_id} lost branch reservation"
4918 )));
4919 }
4920 newly_reserved.push(WriteId::from_generated(write_id.clone()));
4921 }
4922 ("\"publishing\"", Some(stored)) if stored == preparing => {}
4923 ("\"publishing\"", Some(_)) => {
4924 tx.commit().map_err(DbError::from)?;
4925 return Ok(None);
4926 }
4927 _ => {
4928 tx.commit().map_err(DbError::from)?;
4929 return Ok(None);
4930 }
4931 }
4932 let partitions = Self::prepared_store_write_partitions_on(
4933 &tx,
4934 &write_id,
4935 &changeset,
4936 WritePolicy::Serial,
4937 )?;
4938 writes.push(PreparedStoreWrite {
4939 write_id: WriteId::from_generated(write_id),
4940 changeset,
4941 partitions,
4942 inverse_changeset,
4943 base: parsed_base,
4944 blob_facts: serde_json::from_str(&blob_facts).map_err(|error| {
4945 DbError::Message(format!("pending Serial write blob facts: {error}"))
4946 })?,
4947 });
4948 }
4949 if writes.is_empty() {
4950 return Err(DbError::Message("Serial branch reservation is empty".to_string()));
4951 }
4952 tx.commit().map_err(DbError::from)?;
4953 for write_id in newly_reserved {
4954 Self::notify_write_status_in(&statuses, &write_id, WriteStatus::Publishing);
4955 }
4956 Ok(Some(SerialStoreBranchPreparationWork {
4957 branch_id,
4958 base,
4959 writes,
4960 }))
4961 })
4962 .await
4963 }
4964
4965 #[cfg(test)]
4966 pub(crate) async fn enqueue_store_changeset_for_test(
4967 &self,
4968 changeset: Vec<u8>,
4969 ) -> Result<(), DbError> {
4970 let write_id = self.new_write_id();
4971 let blob_decls = self.blob_decls();
4972 let write_policy = self.write_policy();
4973 self.call(move |conn| {
4974 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
4975 let local_stream_id = local_merge_stream_id_on(&tx)?;
4976 let base = match write_policy {
4977 WritePolicy::MergeConcurrent => StoreWriteBase::MergeConcurrent {
4978 dependencies: Self::materialized_frontier_on(&tx, local_stream_id.as_deref())?,
4979 },
4980 WritePolicy::Serial => StoreWriteBase::Serial {
4981 branch_id: PendingBranchId::from_first_write(write_id.clone()),
4982 base: Self::latest_position_for_device_on(&tx, SERIAL_STREAM_ID)?,
4983 },
4984 };
4985 let inverse_changeset = Self::invert_changeset(&changeset)?;
4986 let partitions = vec![gate::AudiencePartition {
4987 audience: crate::sync::circle::Audience::Store,
4988 control: None,
4989 changeset,
4990 }];
4991 let blob_facts = Self::capture_partition_blob_facts_on(&tx, &partitions, &blob_decls)?;
4992 Self::insert_store_write_on(
4993 &tx,
4994 &write_id,
4995 &partitions,
4996 &inverse_changeset,
4997 &base,
4998 &blob_facts,
4999 1,
5000 )?;
5001 tx.commit().map_err(DbError::from)
5002 })
5003 .await
5004 }
5005
5006 pub(crate) async fn prepare_store_write_commit(
5007 &self,
5008 stage: StoreWritePreparation,
5009 ) -> Result<(), DbError> {
5010 let write_id = stage.write_id.clone();
5011 let statuses = self.state.write_statuses.clone();
5012 self.call(move |conn| {
5013 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
5014 let local_device_id: String = tx.query_row(
5015 "SELECT value FROM protocol_state WHERE key = ?1",
5016 [LOCAL_DEVICE_ID_STATE_KEY],
5017 |row| row.get(0),
5018 ).map_err(DbError::from)?;
5019 let root = required_store_root_authority_on(&tx)?;
5020 let (registration_bytes, registration_object): (Vec<u8>, String) = tx.query_row(
5021 "SELECT registration_bytes, registration_object \
5022 FROM store_device_registration_activations WHERE device_id = ?1",
5023 [&local_device_id],
5024 |row| Ok((row.get(0)?, row.get(1)?)),
5025 ).map_err(DbError::from)?;
5026 let registration_ref: StoreDeviceRegistrationRef =
5027 serde_json::from_str(®istration_object).map_err(|error| {
5028 DbError::Message(format!("prepared write exact registration ref: {error}"))
5029 })?;
5030 if registration_ref != stage.commit.value.author_registration
5031 || registration_ref != stage.head.value.author_registration
5032 {
5033 return Err(DbError::Message(
5034 "prepared Store commit/head author registration differs from local activation"
5035 .to_string(),
5036 ));
5037 }
5038 let registration = StoreDeviceRegistration::parse_at(
5039 ®istration_bytes,
5040 &root,
5041 registration_ref.device_id,
5042 ).map_err(|error| DbError::Message(format!(
5043 "prepared write author registration: {error}"
5044 )))?;
5045 registration_ref.verify_registration(®istration)
5046 .map_err(|error| DbError::Message(error.to_string()))?;
5047 let stream_id = AuthorStreamId::store_announcements(&root, ®istration_ref);
5048 let coord = StoreCommitCoord::MergeConcurrent {
5049 stream_id,
5050 sequence: stage.commit.value.seq(),
5051 };
5052 stage.commit.value.verify_at(root.store_root_hash, &coord, ®istration)
5053 .map_err(|error| DbError::Message(format!(
5054 "verify prepared Store commit: {error}"
5055 )))?;
5056 if stage.commit.value.write_id != stage.write_id {
5057 return Err(DbError::Message(
5058 "prepared write id differs from signed commit".to_string(),
5059 ));
5060 }
5061 let commit_ref = StoreBatchCommitRef::from_commit(
5062 &stage.commit.value,
5063 coord,
5064 stage.commit.prepared.reference().clone(),
5065 ).map_err(|error| DbError::Message(error.to_string()))?;
5066 if stage.head.value.commit != commit_ref {
5067 return Err(DbError::Message(
5068 "prepared Store head does not activate the exact prepared commit".to_string(),
5069 ));
5070 }
5071 StoreDeviceHead::parse_at(
5072 &stage.head.value.to_bytes(),
5073 root.store_root_hash,
5074 ®istration,
5075 &commit_ref,
5076 ).map_err(|error| DbError::Message(format!(
5077 "verify prepared Store head: {error}"
5078 )))?;
5079 let (stored_changeset, stored_base, stored_status, stored_preparation): (
5080 Vec<u8>,
5081 String,
5082 String,
5083 Option<String>,
5084 ) = tx
5085 .query_row(
5086 "SELECT changeset, base, status, prepared
5087 FROM store_writes WHERE write_id = ?1",
5088 [stage.write_id.as_str()],
5089 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
5090 )
5091 .map_err(DbError::from)?;
5092 if stored_status != "\"pending\"" || stored_preparation.is_some() {
5093 return Err(DbError::Message(format!(
5094 "write {} is not an unprepared pending write",
5095 stage.write_id
5096 )));
5097 }
5098 let partitions = Self::prepared_store_write_partitions_on(
5099 &tx,
5100 stage.write_id.as_str(),
5101 &stored_changeset,
5102 WritePolicy::MergeConcurrent,
5103 )?;
5104 let stored_base: StoreWriteBase =
5105 serde_json::from_str(&stored_base).map_err(|error| {
5106 DbError::Message(format!("write {} base: {error}", stage.write_id))
5107 })?;
5108 let StoreWriteBase::MergeConcurrent {
5109 dependencies: stored_dependencies,
5110 } = stored_base
5111 else {
5112 return Err(DbError::Message(format!(
5113 "serial write {} reached MergeConcurrent preparation",
5114 stage.write_id
5115 )));
5116 };
5117 let stored_dependencies = CommitFrontier::from_refs(
5118 WritePolicy::MergeConcurrent,
5119 stored_dependencies,
5120 ).map_err(|error| DbError::Message(format!(
5121 "stored write dependencies: {error}"
5122 )))?;
5123 if stored_dependencies.merge_commits().map_err(|error| {
5124 DbError::Message(error.to_string())
5125 })? != stage.commit.value.merge_dependencies().map_err(|error| {
5126 DbError::Message(format!("prepared Store commit policy: {error}"))
5127 })? {
5128 return Err(DbError::Message(format!(
5129 "prepared commit dependencies differ from write {}",
5130 stage.write_id
5131 )));
5132 }
5133 let another_prepared: Option<String> = tx
5134 .query_row(
5135 "SELECT write_id FROM store_writes
5136 WHERE prepared IS NOT NULL AND write_id != ?1
5137 ORDER BY ordinal LIMIT 1",
5138 [stage.write_id.as_str()],
5139 |row| row.get(0),
5140 )
5141 .optional()
5142 .map_err(DbError::from)?;
5143 if let Some(other_write_id) = another_prepared {
5144 return Err(DbError::Message(format!(
5145 "write {other_write_id} already owns Store publication"
5146 )));
5147 }
5148 let durable_predecessor =
5149 Self::latest_position_for_device_on(&tx, &stream_id.to_string())?;
5150 let expected_seq = durable_predecessor
5151 .as_ref()
5152 .map_or(1, |reference| reference.coord.sequence().saturating_add(1));
5153 if stage.commit.value.seq() != expected_seq
5154 || stage.commit.value.order.predecessor() != durable_predecessor.as_ref()
5155 {
5156 return Err(DbError::Message(format!(
5157 "outbound Store commit exact predecessor differs from durable {durable_predecessor:?}"
5158 )));
5159 }
5160
5161 Self::persist_closed_write_objects_on(
5162 &tx,
5163 &stage.write_id,
5164 root.store_root_hash,
5165 &commit_ref,
5166 &stage.commit.value,
5167 stage.commit.prepared.stored_bytes(),
5168 &partitions,
5169 &stage.remote_objects,
5170 &stage.audiences,
5171 )?;
5172 let head_ref = crate::sync::store_commit::StoreDeviceHeadRef {
5173 head_hash: stage.head.value.head_hash(),
5174 object: stage.head.prepared.reference().clone(),
5175 };
5176 let head_remote = RemoteObjectRecord::candidate_activated_store_head(
5177 head_ref,
5178 stage.head.value.to_bytes(),
5179 stage.head.prepared.stored_bytes().to_vec(),
5180 commit_ref.clone(),
5181 )
5182 .map_err(|error| DbError::Message(format!("prepared Store head: {error}")))?;
5183 persist_exact_remote_object_on(&tx, &head_remote, "Store head")?;
5184
5185 let prepared = PreparedStoreWriteState::MergeConcurrent {
5186 commit: DurablePreparedProtocolObject {
5187 semantic_bytes: stage.commit.value.to_bytes(),
5188 prepared: stage.commit.prepared,
5189 },
5190 head: DurablePreparedProtocolObject {
5191 semantic_bytes: stage.head.value.to_bytes(),
5192 prepared: stage.head.prepared,
5193 },
5194 local_cleanup: stage.local_cleanup,
5195 completion: stage.completion,
5196 };
5197 let prepared = serde_json::to_string(&prepared).map_err(|error| {
5198 DbError::Message(format!("serialize prepared Store write: {error}"))
5199 })?;
5200 let status = serde_json::to_string(&WriteStatus::Publishing)
5201 .map_err(|error| DbError::Message(format!("serialize write status: {error}")))?;
5202 let updated = tx
5203 .execute(
5204 "UPDATE store_writes SET prepared = ?2, status = ?3
5205 WHERE write_id = ?1 AND prepared IS NULL AND status = '\"pending\"'",
5206 rusqlite::params![stage.write_id.as_str(), prepared, status],
5207 )
5208 .map_err(DbError::from)?;
5209 if updated != 1 {
5210 return Err(DbError::Message(format!(
5211 "write {} lost pending preparation ownership",
5212 stage.write_id
5213 )));
5214 }
5215 tx.commit().map_err(DbError::from)?;
5216 Self::notify_write_status_in(&statuses, &write_id, WriteStatus::Publishing);
5217 Ok(())
5218 })
5219 .await
5220 }
5221
5222 pub(crate) async fn prepare_merge_candidate_abandonment(
5223 &self,
5224 stage: MergeCandidateAbandonmentPreparation,
5225 ) -> Result<(), DbError> {
5226 if self.write_policy() != WritePolicy::MergeConcurrent {
5227 return Err(DbError::Message(
5228 "Merge candidate abandonment requires MergeConcurrent policy".to_string(),
5229 ));
5230 }
5231 let statuses = self.state.write_statuses.clone();
5232 let notified_write_id = stage.write_id.clone();
5233 self.call(move |conn| {
5234 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
5235 let (raw_status, raw_prepared): (String, String) = tx
5236 .query_row(
5237 "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
5238 [stage.write_id.as_str()],
5239 |row| Ok((row.get(0)?, row.get(1)?)),
5240 )
5241 .map_err(DbError::from)?;
5242 let status: WriteStatus = serde_json::from_str(&raw_status)
5243 .map_err(|error| DbError::Message(format!("Merge abandonment status: {error}")))?;
5244 if !matches!(status, WriteStatus::Blocked(_)) {
5245 return Err(DbError::Message(format!(
5246 "write {} is not blocked",
5247 stage.write_id
5248 )));
5249 }
5250 let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
5251 .map_err(|error| DbError::Message(format!("prepared Merge candidate: {error}")))?;
5252 let PreparedStoreWriteState::MergeConcurrent {
5253 commit: candidate_commit,
5254 head: candidate_head,
5255 local_cleanup,
5256 completion,
5257 } = prepared
5258 else {
5259 return Err(DbError::Message(
5260 "Merge abandonment requires one prepared candidate".to_string(),
5261 ));
5262 };
5263 let candidate =
5264 parse_prepared_merge_candidate_parts_on(&tx, &candidate_commit, &candidate_head)?;
5265 if candidate.commit.write_id != stage.write_id {
5266 return Err(DbError::Message(
5267 "prepared Merge candidate differs from its write identity".to_string(),
5268 ));
5269 }
5270 let root = required_store_root_authority_on(&tx)?;
5271 let registration =
5272 load_activated_registration_on(&tx, &root, &candidate.commit.author_registration)?;
5273 stage
5274 .commit
5275 .value
5276 .verify_at(
5277 root.store_root_hash,
5278 &candidate.reference.coord,
5279 ®istration,
5280 )
5281 .map_err(|error| {
5282 DbError::Message(format!("verify Merge abandonment commit: {error}"))
5283 })?;
5284 if stage.commit.value.write_id != stage.write_id
5285 || stage.commit.value.abandoned_candidates()
5286 != [crate::sync::store_commit::CandidateCleanupManifest {
5287 candidate: crate::sync::store_commit::StoreBatchCommitDeletionTarget {
5288 coord: candidate.reference.coord.clone(),
5289 object: candidate.reference.object.clone(),
5290 canonical_signed_bytes: candidate.canonical_signed_bytes.clone(),
5291 },
5292 }]
5293 {
5294 return Err(DbError::Message(
5295 "Merge abandonment does not name its exact prepared candidate".to_string(),
5296 ));
5297 }
5298 let authority_ref = StoreBatchCommitRef::from_commit(
5299 &stage.commit.value,
5300 candidate.reference.coord.clone(),
5301 stage.commit.prepared.reference().clone(),
5302 )
5303 .map_err(|error| DbError::Message(error.to_string()))?;
5304 if stage.head.value.commit != authority_ref
5305 || stage.head.prepared.reference().slot()
5306 != candidate.head_prepared.reference().slot()
5307 || stage.head.value.successor != candidate.head.successor
5308 || stage.head.value.author_registration != candidate.commit.author_registration
5309 {
5310 return Err(DbError::Message(
5311 "Merge abandonment head differs from the candidate competition point"
5312 .to_string(),
5313 ));
5314 }
5315 StoreDeviceHead::parse_at(
5316 &stage.head.value.to_bytes(),
5317 root.store_root_hash,
5318 ®istration,
5319 &authority_ref,
5320 )
5321 .map_err(|error| DbError::Message(format!("verify Merge abandonment head: {error}")))?;
5322 let authority_commit = RemoteObjectRecord::candidate_commit(
5323 authority_ref.clone(),
5324 stage.commit.value.to_bytes(),
5325 stage.commit.prepared.stored_bytes().to_vec(),
5326 )
5327 .map_err(|error| DbError::Message(format!("Merge abandonment commit: {error}")))?;
5328 persist_exact_remote_object_on(&tx, &authority_commit, "Merge abandonment commit")?;
5329 let authority_head_ref = crate::sync::store_commit::StoreDeviceHeadRef {
5330 head_hash: stage.head.value.head_hash(),
5331 object: stage.head.prepared.reference().clone(),
5332 };
5333 let authority_head = RemoteObjectRecord::candidate_activated_store_head(
5334 authority_head_ref,
5335 stage.head.value.to_bytes(),
5336 stage.head.prepared.stored_bytes().to_vec(),
5337 authority_ref,
5338 )
5339 .map_err(|error| DbError::Message(format!("Merge abandonment head: {error}")))?;
5340 persist_exact_remote_object_on(&tx, &authority_head, "Merge abandonment head")?;
5341 let replacement = PreparedStoreWriteState::MergeAbandonment {
5342 candidate_commit,
5343 candidate_head,
5344 authority_commit: DurablePreparedProtocolObject {
5345 semantic_bytes: stage.commit.value.to_bytes(),
5346 prepared: stage.commit.prepared,
5347 },
5348 authority_head: DurablePreparedProtocolObject {
5349 semantic_bytes: stage.head.value.to_bytes(),
5350 prepared: stage.head.prepared,
5351 },
5352 outcome: MergeAbandonmentOutcome::Prepared,
5353 local_cleanup,
5354 completion,
5355 };
5356 let replacement = serde_json::to_string(&replacement).map_err(|error| {
5357 DbError::Message(format!("serialize Merge abandonment: {error}"))
5358 })?;
5359 let publishing = serde_json::to_string(&WriteStatus::Publishing).map_err(|error| {
5360 DbError::Message(format!("serialize Merge abandonment status: {error}"))
5361 })?;
5362 let updated = tx
5363 .execute(
5364 "UPDATE store_writes SET prepared = ?2, status = ?3
5365 WHERE write_id = ?1 AND prepared = ?4
5366 AND json_extract(status, '$.blocked') IS NOT NULL",
5367 rusqlite::params![
5368 stage.write_id.as_str(),
5369 replacement,
5370 publishing,
5371 raw_prepared
5372 ],
5373 )
5374 .map_err(DbError::from)?;
5375 if updated != 1 {
5376 return Err(DbError::Message(
5377 "blocked Merge candidate changed during abandonment preparation".to_string(),
5378 ));
5379 }
5380 tx.commit().map_err(DbError::from)?;
5381 Self::notify_write_status_in(&statuses, ¬ified_write_id, WriteStatus::Publishing);
5382 Ok(())
5383 })
5384 .await
5385 }
5386
5387 pub(crate) async fn prepare_serial_candidate_abandonment(
5388 &self,
5389 stage: SerialCandidateAbandonmentPreparation,
5390 ) -> Result<(), DbError> {
5391 if self.write_policy() != WritePolicy::Serial {
5392 return Err(DbError::Message(
5393 "Serial candidate abandonment requires the Serial write policy".to_string(),
5394 ));
5395 }
5396 self.call(move |conn| {
5397 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
5398 let existing: Option<String> = tx
5399 .query_row(
5400 "SELECT value FROM protocol_state WHERE key = ?1",
5401 [SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY],
5402 |row| row.get(0),
5403 )
5404 .optional()
5405 .map_err(DbError::from)?;
5406 if existing.is_some() {
5407 return Err(DbError::Message(
5408 "a Serial candidate abandonment is already prepared".to_string(),
5409 ));
5410 }
5411 let (raw_base, raw_prepared): (String, String) = tx
5412 .query_row(
5413 "SELECT base, prepared FROM store_writes
5414 WHERE prepared IS NOT NULL AND status = '\"publishing\"'
5415 ORDER BY ordinal LIMIT 1",
5416 [],
5417 |row| Ok((row.get(0)?, row.get(1)?)),
5418 )
5419 .map_err(DbError::from)?;
5420 let StoreWriteBase::Serial { branch_id, base } = serde_json::from_str(&raw_base)
5421 .map_err(|error| {
5422 DbError::Message(format!("Serial abandonment branch base: {error}"))
5423 })?
5424 else {
5425 return Err(DbError::Message(
5426 "Merge branch reached Serial candidate abandonment".to_string(),
5427 ));
5428 };
5429 if branch_id != stage.branch_id {
5430 return Err(DbError::Message(
5431 "Serial abandonment names another pending branch".to_string(),
5432 ));
5433 }
5434 let prepared: PreparedStoreWriteState =
5435 serde_json::from_str(&raw_prepared).map_err(|error| {
5436 DbError::Message(format!("prepared Serial abandonment candidate: {error}"))
5437 })?;
5438 let PreparedStoreWriteState::Serial {
5439 base_head_bytes,
5440 base_head_version,
5441 commit: candidate_commit,
5442 ..
5443 } = &prepared
5444 else {
5445 return Err(DbError::Message(
5446 "Serial abandonment requires an exact prepared branch".to_string(),
5447 ));
5448 };
5449 let base_head = VersionedObject {
5450 bytes: base_head_bytes.clone().ok_or_else(|| {
5451 DbError::Message(
5452 "Serial abandonment base has no signed coordination head".to_string(),
5453 )
5454 })?,
5455 version: base_head_version.clone().ok_or_else(|| {
5456 DbError::Message(
5457 "Serial abandonment base has no provider version receipt".to_string(),
5458 )
5459 })?,
5460 };
5461 let candidate = parse_prepared_serial_candidate(&raw_prepared)?
5462 .expect("matched exact Serial candidate");
5463 if candidate.reference != stage.candidate {
5464 return Err(DbError::Message(
5465 "Serial abandonment target differs from the branch first candidate".to_string(),
5466 ));
5467 }
5468 let root = required_store_root_authority_on(&tx)?;
5469 let registration =
5470 load_activated_registration_on(&tx, &root, &candidate.commit.author_registration)?;
5471 stage
5472 .commit
5473 .value
5474 .verify_at(
5475 root.store_root_hash,
5476 &candidate.reference.coord,
5477 ®istration,
5478 )
5479 .map_err(|error| {
5480 DbError::Message(format!("verify Serial abandonment commit: {error}"))
5481 })?;
5482 if stage.commit.value.write_id != candidate.commit.write_id
5483 || stage.commit.value.order != candidate.commit.order
5484 || stage.commit.value.abandoned_candidates()
5485 != [crate::sync::store_commit::CandidateCleanupManifest {
5486 candidate: crate::sync::store_commit::StoreBatchCommitDeletionTarget {
5487 coord: candidate.reference.coord.clone(),
5488 object: candidate.reference.object.clone(),
5489 canonical_signed_bytes: candidate.canonical_signed_bytes.clone(),
5490 },
5491 }]
5492 {
5493 return Err(DbError::Message(
5494 "Serial abandonment does not replace its exact first candidate".to_string(),
5495 ));
5496 }
5497 let authority_ref = StoreBatchCommitRef::from_commit(
5498 &stage.commit.value,
5499 candidate.reference.coord.clone(),
5500 stage.commit.prepared.reference().clone(),
5501 )
5502 .map_err(|error| DbError::Message(error.to_string()))?;
5503 let parsed_head =
5504 StoreSerialHead::parse(&stage.head.to_bytes(), root.store_root_hash, ®istration)
5505 .map_err(|error| {
5506 DbError::Message(format!("verify Serial abandonment head: {error}"))
5507 })?;
5508 if parsed_head != stage.head
5509 || !matches!(
5510 &stage.head.state,
5511 StoreSerialHeadState::Commit {
5512 author_registration,
5513 commit,
5514 } if author_registration == &candidate.commit.author_registration
5515 && commit == &authority_ref
5516 )
5517 {
5518 return Err(DbError::Message(
5519 "Serial abandonment head does not activate its exact authority".to_string(),
5520 ));
5521 }
5522 let (raw_tip_base, raw_tip_prepared): (String, String) = tx
5523 .query_row(
5524 "SELECT base, prepared FROM store_writes
5525 WHERE prepared IS NOT NULL AND status = '\"publishing\"'
5526 ORDER BY ordinal DESC LIMIT 1",
5527 [],
5528 |row| Ok((row.get(0)?, row.get(1)?)),
5529 )
5530 .map_err(DbError::from)?;
5531 let StoreWriteBase::Serial {
5532 branch_id: tip_branch_id,
5533 base: tip_base,
5534 } = serde_json::from_str(&raw_tip_base).map_err(|error| {
5535 DbError::Message(format!("Serial abandonment tip branch base: {error}"))
5536 })?
5537 else {
5538 return Err(DbError::Message(
5539 "Merge tip reached Serial candidate abandonment".to_string(),
5540 ));
5541 };
5542 let tip_prepared: PreparedStoreWriteState = serde_json::from_str(&raw_tip_prepared)
5543 .map_err(|error| {
5544 DbError::Message(format!("prepared Serial abandonment tip: {error}"))
5545 })?;
5546 let PreparedStoreWriteState::Serial {
5547 tip_head_bytes: Some(tip_head_bytes),
5548 ..
5549 } = tip_prepared
5550 else {
5551 return Err(DbError::Message(
5552 "Serial abandonment branch has no activating tip head".to_string(),
5553 ));
5554 };
5555 if tip_branch_id != branch_id
5556 || tip_base != base
5557 || tip_head_bytes != stage.original_head_bytes
5558 {
5559 return Err(DbError::Message(
5560 "Serial abandonment original head differs from its prepared branch".to_string(),
5561 ));
5562 }
5563 let authority_remote = RemoteObjectRecord::candidate_commit(
5564 authority_ref,
5565 stage.commit.value.to_bytes(),
5566 stage.commit.prepared.stored_bytes().to_vec(),
5567 )
5568 .map_err(|error| DbError::Message(format!("Serial abandonment commit: {error}")))?;
5569 persist_exact_remote_object_on(&tx, &authority_remote, "Serial abandonment commit")?;
5570 let durable = DurableSerialCandidateAbandonment {
5571 branch_id,
5572 base,
5573 base_head,
5574 candidate: candidate.reference,
5575 commit: DurablePreparedProtocolObject {
5576 semantic_bytes: stage.commit.value.to_bytes(),
5577 prepared: stage.commit.prepared,
5578 },
5579 head_bytes: stage.head.to_bytes(),
5580 original_head_bytes: stage.original_head_bytes,
5581 };
5582 let durable = serde_json::to_string(&durable).map_err(|error| {
5583 DbError::Message(format!("serialize Serial candidate abandonment: {error}"))
5584 })?;
5585 tx.execute(
5586 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
5587 (SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY, durable),
5588 )
5589 .map_err(DbError::from)?;
5590 if candidate_commit.prepared.reference() != &stage.candidate.object {
5591 return Err(DbError::Message(
5592 "Serial abandonment candidate storage identity changed".to_string(),
5593 ));
5594 }
5595 tx.commit().map_err(DbError::from)
5596 })
5597 .await
5598 }
5599
5600 pub(crate) async fn prepare_serial_store_branch_commit(
5601 &self,
5602 stage: SerialStoreWritePreparation,
5603 ) -> Result<(), DbError> {
5604 if self.write_policy() != WritePolicy::Serial {
5605 return Err(DbError::Message(
5606 "Serial branch preparation requires the Serial write policy".to_string(),
5607 ));
5608 }
5609 if stage.writes.is_empty() {
5610 return Err(DbError::Message(
5611 "prepared Serial branch is empty".to_string(),
5612 ));
5613 }
5614 self.call(move |conn| {
5615 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
5616 let local_device_id: String = tx
5617 .query_row(
5618 "SELECT value FROM protocol_state WHERE key = ?1",
5619 [LOCAL_DEVICE_ID_STATE_KEY],
5620 |row| row.get(0),
5621 )
5622 .map_err(DbError::from)?;
5623 let root = required_store_root_authority_on(&tx)?;
5624 let expected_base = StoreWriteBase::Serial {
5625 branch_id: stage.branch_id.clone(),
5626 base: stage.base.clone(),
5627 };
5628 if stage.base_head_bytes.is_some() != stage.base_head_version.is_some() {
5629 return Err(DbError::Message(
5630 "Serial base head bytes and opaque version receipt must be present together"
5631 .to_string(),
5632 ));
5633 }
5634 match stage.base_head_bytes.as_deref() {
5635 Some(bytes) => {
5636 let unverified: StoreSerialHead = serde_json::from_slice(bytes)
5637 .map_err(|error| DbError::Message(format!("Serial base head: {error}")))?;
5638 let executor_ref = match &unverified.state {
5639 StoreSerialHeadState::Genesis {
5640 founder_registration,
5641 ..
5642 } => founder_registration,
5643 StoreSerialHeadState::Commit {
5644 author_registration,
5645 ..
5646 } => author_registration,
5647 };
5648 let executor = load_activated_registration_on(&tx, &root, executor_ref)?;
5649 let verified = StoreSerialHead::parse(bytes, root.store_root_hash, &executor)
5650 .map_err(|error| {
5651 DbError::Message(format!("verify Serial base head: {error}"))
5652 })?;
5653 let base_ref = match verified.state {
5654 StoreSerialHeadState::Genesis {
5655 root: head_root,
5656 founder_registration,
5657 } => {
5658 if head_root != root || founder_registration != executor_ref.clone() {
5659 return Err(DbError::Message(
5660 "Serial genesis head differs from exact Store authority"
5661 .to_string(),
5662 ));
5663 }
5664 None
5665 }
5666 StoreSerialHeadState::Commit { commit, .. } => Some(commit),
5667 };
5668 if base_ref != stage.base {
5669 return Err(DbError::Message(
5670 "Serial branch base differs from the exact observed head".to_string(),
5671 ));
5672 }
5673 }
5674 None if stage.base.is_some() => {
5675 return Err(DbError::Message(
5676 "Serial branch has a base commit without observed head evidence"
5677 .to_string(),
5678 ));
5679 }
5680 None => {}
5681 }
5682 let head_bytes = stage.head.to_bytes();
5683 let mut predecessor = stage.base.clone();
5684 let mut tip_ref = None;
5685 let mut tip_registration = None;
5686 for (index, write) in stage.writes.iter().enumerate() {
5687 if write.commit.value.write_id != write.write_id
5688 || write.commit.value.author_registration.device_id.to_string()
5689 != local_device_id
5690 {
5691 return Err(DbError::Message(format!(
5692 "prepared Serial write {} identity differs from its commit",
5693 write.write_id
5694 )));
5695 }
5696 let expected_seq = predecessor
5697 .as_ref()
5698 .map_or(1, |reference| reference.coord.sequence().saturating_add(1));
5699 let coord = StoreCommitCoord::Serial {
5700 sequence: expected_seq,
5701 };
5702 let registration = load_activated_registration_on(
5703 &tx,
5704 &root,
5705 &write.commit.value.author_registration,
5706 )?;
5707 write
5708 .commit
5709 .value
5710 .verify_at(root.store_root_hash, &coord, ®istration)
5711 .map_err(|error| {
5712 DbError::Message(format!("verify prepared Serial commit: {error}"))
5713 })?;
5714 let order_matches = match (&predecessor, &write.commit.value.order) {
5715 (
5716 Some(expected),
5717 crate::sync::store_commit::StoreCommitOrder::Serial {
5718 predecessor: StoreSerialPredecessor::Commit(actual),
5719 ..
5720 },
5721 ) => actual == expected,
5722 (
5723 None,
5724 crate::sync::store_commit::StoreCommitOrder::Serial {
5725 predecessor:
5726 StoreSerialPredecessor::Genesis {
5727 root: commit_root,
5728 founder_registration,
5729 },
5730 ..
5731 },
5732 ) => {
5733 commit_root == &root
5734 && founder_registration == &write.commit.value.author_registration
5735 }
5736 _ => false,
5737 };
5738 if !order_matches {
5739 return Err(DbError::Message(format!(
5740 "prepared Serial commit {} has the wrong exact predecessor",
5741 write.write_id
5742 )));
5743 }
5744 let commit_ref = StoreBatchCommitRef::from_commit(
5745 &write.commit.value,
5746 coord,
5747 write.commit.prepared.reference().clone(),
5748 )
5749 .map_err(|error| DbError::Message(error.to_string()))?;
5750 let (stored_changeset, stored_base, status, prepared): (
5751 Vec<u8>,
5752 String,
5753 String,
5754 String,
5755 ) = tx
5756 .query_row(
5757 "SELECT changeset, base, status, prepared FROM store_writes
5758 WHERE write_id = ?1",
5759 [write.write_id.as_str()],
5760 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
5761 )
5762 .map_err(DbError::from)?;
5763 let stored_base: StoreWriteBase = serde_json::from_str(&stored_base)
5764 .map_err(|error| DbError::Message(format!("stored Serial base: {error}")))?;
5765 let stored_prepared: PreparedStoreWriteState = serde_json::from_str(&prepared)
5766 .map_err(|error| {
5767 DbError::Message(format!("stored Serial reservation: {error}"))
5768 })?;
5769 if stored_base != expected_base
5770 || status != "\"publishing\""
5771 || !matches!(stored_prepared, PreparedStoreWriteState::SerialPreparing)
5772 {
5773 return Err(DbError::Message(format!(
5774 "Serial write {} no longer owns its exact branch reservation",
5775 write.write_id
5776 )));
5777 }
5778 let partitions = Self::prepared_store_write_partitions_on(
5779 &tx,
5780 write.write_id.as_str(),
5781 &stored_changeset,
5782 WritePolicy::Serial,
5783 )?;
5784 Self::persist_closed_write_objects_on(
5785 &tx,
5786 &write.write_id,
5787 root.store_root_hash,
5788 &commit_ref,
5789 &write.commit.value,
5790 write.commit.prepared.stored_bytes(),
5791 &partitions,
5792 &write.remote_objects,
5793 &write.audiences,
5794 )?;
5795 let tip_head_bytes = (index + 1 == stage.writes.len()).then(|| head_bytes.clone());
5796 let durable = PreparedStoreWriteState::Serial {
5797 base_head_bytes: stage.base_head_bytes.clone(),
5798 base_head_version: stage.base_head_version.clone(),
5799 commit: DurablePreparedProtocolObject {
5800 semantic_bytes: write.commit.value.to_bytes(),
5801 prepared: write.commit.prepared.clone(),
5802 },
5803 tip_head_bytes,
5804 local_cleanup: write.local_cleanup.clone(),
5805 completion: write.completion.clone(),
5806 };
5807 let durable = serde_json::to_string(&durable).map_err(|error| {
5808 DbError::Message(format!("serialize prepared Serial write: {error}"))
5809 })?;
5810 let updated = tx
5811 .execute(
5812 "UPDATE store_writes SET prepared = ?2
5813 WHERE write_id = ?1 AND status = '\"publishing\"'",
5814 rusqlite::params![write.write_id.as_str(), durable],
5815 )
5816 .map_err(DbError::from)?;
5817 if updated != 1 {
5818 return Err(DbError::Message(format!(
5819 "Serial write {} lost exact preparation ownership",
5820 write.write_id
5821 )));
5822 }
5823 predecessor = Some(commit_ref.clone());
5824 tip_ref = Some(commit_ref);
5825 tip_registration = Some(registration);
5826 }
5827 let tip_ref = tip_ref.expect("nonempty checked above");
5828 let tip_registration = tip_registration.expect("nonempty checked above");
5829 let parsed_head =
5830 StoreSerialHead::parse(&head_bytes, root.store_root_hash, &tip_registration)
5831 .map_err(|error| {
5832 DbError::Message(format!("verify prepared Serial head: {error}"))
5833 })?;
5834 let tip_author_ref = &stage
5835 .writes
5836 .last()
5837 .expect("nonempty checked above")
5838 .commit
5839 .value
5840 .author_registration;
5841 if parsed_head != stage.head
5842 || !matches!(
5843 &stage.head.state,
5844 StoreSerialHeadState::Commit {
5845 author_registration,
5846 commit,
5847 } if author_registration == tip_author_ref
5848 && commit == &tip_ref
5849 )
5850 {
5851 return Err(DbError::Message(
5852 "prepared Serial head does not activate the exact branch tip".to_string(),
5853 ));
5854 }
5855 let reserved_count: i64 = tx
5856 .query_row(
5857 "SELECT COUNT(*) FROM store_writes
5858 WHERE status = '\"publishing\"' AND json_type(base, '$.serial') IS NOT NULL",
5859 [],
5860 |row| row.get(0),
5861 )
5862 .map_err(DbError::from)?;
5863 if reserved_count
5864 != i64::try_from(stage.writes.len()).map_err(|_| {
5865 DbError::Message("Serial branch length exceeds SQLite integer".into())
5866 })?
5867 {
5868 return Err(DbError::Message(format!(
5869 "prepared Serial branch contains {} writes but {reserved_count} are reserved",
5870 stage.writes.len()
5871 )));
5872 }
5873 tx.commit().map_err(DbError::from)
5874 })
5875 .await
5876 }
5877
5878 fn write_ids_matching_serial_base<I>(
5879 rows: I,
5880 expected_base: &StoreWriteBase,
5881 ) -> Result<Vec<WriteId>, DbError>
5882 where
5883 I: IntoIterator<Item = rusqlite::Result<(String, String)>>,
5884 {
5885 let mut write_ids = Vec::new();
5886 for row in rows {
5887 let (write_id, raw_base) = row.map_err(DbError::from)?;
5888 let stored_base: StoreWriteBase = serde_json::from_str(&raw_base)
5889 .map_err(|error| DbError::Message(format!("stored Serial base: {error}")))?;
5890 if &stored_base == expected_base {
5891 write_ids.push(WriteId::from_generated(write_id));
5892 }
5893 }
5894 Ok(write_ids)
5895 }
5896
5897 pub(crate) async fn release_serial_store_branch_reservation(
5898 &self,
5899 branch_id: PendingBranchId,
5900 base: Option<StoreBatchCommitRef>,
5901 status: WriteStatus,
5902 ) -> Result<(), DbError> {
5903 if !matches!(status, WriteStatus::Pending | WriteStatus::Blocked(_)) {
5904 return Err(DbError::Message(
5905 "Serial preparation can only return a reservation to pending or blocked"
5906 .to_string(),
5907 ));
5908 }
5909 let statuses = self.state.write_statuses.clone();
5910 self.call(move |conn| {
5911 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
5912 let expected_base = StoreWriteBase::Serial { branch_id, base };
5913 let preparing = serde_json::to_string(&PreparedStoreWriteState::SerialPreparing)
5914 .map_err(|error| {
5915 DbError::Message(format!("serialize Serial reservation: {error}"))
5916 })?;
5917 let status_json = serde_json::to_string(&status)
5918 .map_err(|error| DbError::Message(format!("serialize write status: {error}")))?;
5919 let mut statement = tx
5920 .prepare(
5921 "SELECT write_id, base FROM store_writes
5922 WHERE status = '\"publishing\"' AND prepared = ?1
5923 ORDER BY ordinal",
5924 )
5925 .map_err(DbError::from)?;
5926 let rows = statement
5927 .query_map([&preparing], |row| {
5928 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
5929 })
5930 .map_err(DbError::from)?;
5931 let write_ids = Self::write_ids_matching_serial_base(rows, &expected_base)?;
5932 drop(statement);
5933 if write_ids.is_empty() {
5934 return Err(DbError::Message(
5935 "reserved Serial branch disappeared during preparation".to_string(),
5936 ));
5937 }
5938 for write_id in &write_ids {
5939 let updated = tx
5940 .execute(
5941 "UPDATE store_writes SET status = ?2, prepared = NULL
5942 WHERE write_id = ?1 AND status = '\"publishing\"' AND prepared = ?3",
5943 rusqlite::params![write_id.as_str(), &status_json, &preparing],
5944 )
5945 .map_err(DbError::from)?;
5946 if updated != 1 {
5947 return Err(DbError::Message(format!(
5948 "reserved Serial write {write_id} disappeared during release"
5949 )));
5950 }
5951 }
5952 tx.commit().map_err(DbError::from)?;
5953 for write_id in write_ids {
5954 Self::notify_write_status_in(&statuses, &write_id, status.clone());
5955 }
5956 Ok(())
5957 })
5958 .await
5959 }
5960
5961 pub(crate) async fn oldest_prepared_store_write(
5962 &self,
5963 ) -> Result<Option<PreparedStoreWriteCommit>, DbError> {
5964 let loaded = self
5965 .call(|conn| {
5966 let row = conn
5967 .query_row(
5968 "SELECT write_id, changeset, base, prepared FROM store_writes
5969 WHERE prepared IS NOT NULL
5970 AND status = '\"publishing\"'
5971 ORDER BY ordinal LIMIT 1",
5972 [],
5973 |row| {
5974 Ok((
5975 row.get::<_, String>(0)?,
5976 row.get::<_, Vec<u8>>(1)?,
5977 row.get::<_, String>(2)?,
5978 row.get::<_, String>(3)?,
5979 ))
5980 },
5981 )
5982 .optional()
5983 .map_err(DbError::from)?;
5984 row.map(|(write_id, stored_changeset, base, prepared)| {
5985 let prepared: PreparedStoreWriteState = serde_json::from_str(&prepared)
5986 .map_err(|error| {
5987 DbError::Message(format!("prepared Store write: {error}"))
5988 })?;
5989 let (commit, head, graph_commit) = match &prepared {
5990 PreparedStoreWriteState::MergeConcurrent { commit, head, .. } => {
5991 (commit, head, None)
5992 }
5993 PreparedStoreWriteState::MergeAbandonment {
5994 candidate_commit,
5995 authority_commit,
5996 authority_head,
5997 ..
5998 } => (authority_commit, authority_head, Some(candidate_commit)),
5999 PreparedStoreWriteState::SerialPreparing
6000 | PreparedStoreWriteState::Serial { .. } => {
6001 return Err(DbError::Message(
6002 "serial branch reached MergeConcurrent publication".to_string(),
6003 ));
6004 }
6005 };
6006 let write_id = WriteId::from_generated(write_id);
6007 let unverified_commit: StoreBatchCommit =
6008 serde_json::from_slice(&commit.semantic_bytes).map_err(|error| {
6009 DbError::Message(format!("prepared Store commit: {error}"))
6010 })?;
6011 if unverified_commit.write_id != write_id {
6012 return Err(DbError::Message(
6013 "prepared write id differs from signed commit".to_string(),
6014 ));
6015 }
6016 let root = required_store_root_authority_on(conn)?;
6017 let registration_ref = &unverified_commit.author_registration;
6018 let (registration_bytes, stored_registration_ref): (Vec<u8>, String) = conn
6019 .query_row(
6020 "SELECT registration_bytes, registration_object \
6021 FROM store_device_registration_activations \
6022 WHERE device_id = ?1 AND registration_hash = ?2",
6023 (
6024 registration_ref.device_id.to_string(),
6025 registration_ref.registration_hash.to_string(),
6026 ),
6027 |row| Ok((row.get(0)?, row.get(1)?)),
6028 )
6029 .map_err(DbError::from)?;
6030 let stored_registration_ref: StoreDeviceRegistrationRef =
6031 serde_json::from_str(&stored_registration_ref).map_err(|error| {
6032 DbError::Message(format!("prepared write registration ref: {error}"))
6033 })?;
6034 if stored_registration_ref != *registration_ref {
6035 return Err(DbError::Message(
6036 "prepared commit registration differs from its activation".to_string(),
6037 ));
6038 }
6039 let registration = StoreDeviceRegistration::parse_at(
6040 ®istration_bytes,
6041 &root,
6042 registration_ref.device_id,
6043 )
6044 .map_err(|error| {
6045 DbError::Message(format!("prepared write registration: {error}"))
6046 })?;
6047 let stream_id = AuthorStreamId::store_announcements(&root, registration_ref);
6048 let coord = StoreCommitCoord::MergeConcurrent {
6049 stream_id,
6050 sequence: unverified_commit.seq(),
6051 };
6052 let commit_value = StoreBatchCommit::parse_at(
6053 &commit.semantic_bytes,
6054 root.store_root_hash,
6055 &coord,
6056 ®istration,
6057 )
6058 .map_err(|error| {
6059 DbError::Message(format!("verify prepared Store commit: {error}"))
6060 })?;
6061 let commit_ref = StoreBatchCommitRef::from_commit(
6062 &commit_value,
6063 coord,
6064 commit.prepared.reference().clone(),
6065 )
6066 .map_err(|error| DbError::Message(error.to_string()))?;
6067 let head_value = StoreDeviceHead::parse_at(
6068 &head.semantic_bytes,
6069 root.store_root_hash,
6070 ®istration,
6071 &commit_ref,
6072 )
6073 .map_err(|error| {
6074 DbError::Message(format!("verify prepared Store head: {error}"))
6075 })?;
6076 let base: StoreWriteBase = serde_json::from_str(&base).map_err(|error| {
6077 DbError::Message(format!("prepared write base: {error}"))
6078 })?;
6079 let StoreWriteBase::MergeConcurrent { dependencies } = base else {
6080 return Err(DbError::Message(
6081 "serial base reached MergeConcurrent publication".to_string(),
6082 ));
6083 };
6084 let dependencies =
6085 CommitFrontier::from_refs(WritePolicy::MergeConcurrent, dependencies)
6086 .map_err(|error| {
6087 DbError::Message(format!("prepared dependency frontier: {error}"))
6088 })?;
6089 if dependencies
6090 .merge_commits()
6091 .map_err(|error| DbError::Message(error.to_string()))?
6092 != commit_value.merge_dependencies().map_err(|error| {
6093 DbError::Message(format!("prepared Store commit policy: {error}"))
6094 })?
6095 {
6096 return Err(DbError::Message(
6097 "prepared commit differs from its write dependency frontier"
6098 .to_string(),
6099 ));
6100 }
6101 let partitions = Self::prepared_store_write_partitions_on(
6102 conn,
6103 write_id.as_str(),
6104 &stored_changeset,
6105 WritePolicy::MergeConcurrent,
6106 )?;
6107 let audiences = load_prepared_audience_objects_on(conn, &write_id)?;
6108 let graph_commit = match graph_commit {
6109 Some(graph_commit) => {
6110 let candidate = parse_prepared_merge_candidate_parts_on(
6111 conn,
6112 graph_commit,
6113 match &prepared {
6114 PreparedStoreWriteState::MergeAbandonment {
6115 candidate_head,
6116 ..
6117 } => candidate_head,
6118 _ => unreachable!("matched Merge abandonment"),
6119 },
6120 )?;
6121 candidate.commit
6122 }
6123 None => commit_value.clone(),
6124 };
6125 let expected_package_count =
6126 usize::from(graph_commit.store_package().is_some())
6127 .checked_add(graph_commit.circle_packages().len())
6128 .ok_or_else(|| {
6129 DbError::Message("package count overflow".to_string())
6130 })?;
6131 if audiences.packages.len() != expected_package_count
6132 || audiences.packages.len()
6133 != usize::from(partitions.store.is_some()) + partitions.circles.len()
6134 {
6135 return Err(DbError::Message(
6136 "prepared package indexes do not exactly cover commit audiences"
6137 .to_string(),
6138 ));
6139 }
6140 for package in &audiences.packages {
6141 let value = package.package();
6142 if value.write_id() != &write_id
6143 || value.commit_coord() != &commit_ref.coord
6144 || value.candidate_family() != commit_value.candidate_family()
6145 {
6146 return Err(DbError::Message(
6147 "indexed audience package differs from its exact commit"
6148 .to_string(),
6149 ));
6150 }
6151 let expected_object = match value.audience() {
6152 crate::sync::audience_package::PackageAudience::Store => {
6153 graph_commit
6154 .verify_store_package(package.semantic_bytes())
6155 .map_err(|error| DbError::Message(error.to_string()))?;
6156 &graph_commit
6157 .store_package()
6158 .as_ref()
6159 .expect("verified present")
6160 .object
6161 }
6162 crate::sync::audience_package::PackageAudience::Circle {
6163 circle_id,
6164 ..
6165 } => {
6166 graph_commit
6167 .verify_circle_package(*circle_id, package.semantic_bytes())
6168 .map_err(|error| DbError::Message(error.to_string()))?;
6169 &graph_commit
6170 .circle_packages()
6171 .iter()
6172 .find(|entry| entry.circle_id == *circle_id)
6173 .expect("verified present")
6174 .package
6175 .object
6176 }
6177 };
6178 if package.object() != expected_object {
6179 return Err(DbError::Message(
6180 "indexed audience package exact object differs from its commit"
6181 .to_string(),
6182 ));
6183 }
6184 }
6185 for package in &audiences.packages {
6186 let audience = package.package().audience().remote_audience();
6187 for binding in package.package().blob_bindings() {
6188 if !audiences.blobs.iter().any(|blob| {
6189 blob.audience() == &audience && blob.blob() == binding.blob()
6190 }) {
6191 return Err(DbError::Message(
6192 "prepared package blob binding has no exact blob index"
6193 .to_string(),
6194 ));
6195 }
6196 }
6197 }
6198 for blob in &audiences.blobs {
6199 if !audiences.packages.iter().any(|package| {
6200 package.package().audience().remote_audience() == *blob.audience()
6201 && package
6202 .package()
6203 .blob_bindings()
6204 .iter()
6205 .any(|binding| binding.blob() == blob.blob())
6206 }) {
6207 return Err(DbError::Message(
6208 "prepared blob index has no exact package binding".to_string(),
6209 ));
6210 }
6211 }
6212 Ok(PreparedStoreWriteCommit {
6213 audiences,
6214 commit: ExactProtocolObject {
6215 value: commit_value,
6216 bytes: commit.semantic_bytes.clone(),
6217 object: commit.prepared.reference().clone(),
6218 prepared: commit.prepared.clone(),
6219 },
6220 head: ExactProtocolObject {
6221 value: head_value,
6222 bytes: head.semantic_bytes.clone(),
6223 object: head.prepared.reference().clone(),
6224 prepared: head.prepared.clone(),
6225 },
6226 })
6227 })
6228 .transpose()
6229 })
6230 .await?;
6231 if let Some(batch) = &loaded {
6232 for blob in &batch.audiences.blobs {
6233 if let Some(spool_path) = blob.spool_path() {
6234 crate::local_blob::verify_exact_file(blob.blob().object(), spool_path)
6235 .await
6236 .map_err(|error| {
6237 DbError::Message(format!("prepared blob spool: {error}"))
6238 })?;
6239 }
6240 }
6241 }
6242 Ok(loaded)
6243 }
6244
6245 pub(crate) async fn prepared_serial_store_branch(
6246 &self,
6247 ) -> Result<Option<PreparedSerialStoreBranch>, DbError> {
6248 let loaded = self
6249 .call(|conn| {
6250 let root = required_store_root_authority_on(conn)?;
6251 let mut statement = conn
6252 .prepare(
6253 "SELECT write_id, changeset, base, prepared FROM store_writes
6254 WHERE prepared IS NOT NULL AND status = '\"publishing\"'
6255 ORDER BY ordinal",
6256 )
6257 .map_err(DbError::from)?;
6258 let rows = statement
6259 .query_map([], |row| {
6260 Ok((
6261 row.get::<_, String>(0)?,
6262 row.get::<_, Vec<u8>>(1)?,
6263 row.get::<_, String>(2)?,
6264 row.get::<_, String>(3)?,
6265 ))
6266 })
6267 .map_err(DbError::from)?
6268 .collect::<Result<Vec<_>, _>>()
6269 .map_err(DbError::from)?;
6270 drop(statement);
6271 let mut branch_id = None;
6272 let mut base = None;
6273 let mut base_head_bytes: Option<Option<Vec<u8>>> = None;
6274 let mut base_head_version: Option<Option<VersionToken>> = None;
6275 let mut writes = Vec::new();
6276 let mut head = None;
6277 let mut predecessor: Option<StoreBatchCommitRef> = None;
6278 for row in rows {
6279 let (write_id, stored_changeset, raw_base, prepared) = row;
6280 let prepared: PreparedStoreWriteState = serde_json::from_str(&prepared)
6281 .map_err(|error| {
6282 DbError::Message(format!("prepared Serial write: {error}"))
6283 })?;
6284 if matches!(prepared, PreparedStoreWriteState::SerialPreparing) {
6285 if writes.is_empty() {
6286 return Ok(None);
6287 }
6288 return Err(DbError::Message(
6289 "Serial branch mixes reserved and exact prepared writes".to_string(),
6290 ));
6291 }
6292 let PreparedStoreWriteState::Serial {
6293 base_head_bytes: row_base_head_bytes,
6294 base_head_version: row_base_head_version,
6295 commit,
6296 tip_head_bytes,
6297 ..
6298 } = prepared
6299 else {
6300 return Err(DbError::Message(
6301 "MergeConcurrent write reached Serial publication".to_string(),
6302 ));
6303 };
6304 let StoreWriteBase::Serial {
6305 branch_id: row_branch_id,
6306 base: row_base,
6307 } = serde_json::from_str(&raw_base).map_err(|error| {
6308 DbError::Message(format!("prepared Serial base: {error}"))
6309 })?
6310 else {
6311 return Err(DbError::Message(
6312 "MergeConcurrent base reached Serial publication".to_string(),
6313 ));
6314 };
6315 if branch_id
6316 .as_ref()
6317 .is_some_and(|value| value != &row_branch_id)
6318 || base.as_ref().is_some_and(|value| value != &row_base)
6319 || base_head_bytes
6320 .as_ref()
6321 .is_some_and(|value| value != &row_base_head_bytes)
6322 || base_head_version
6323 .as_ref()
6324 .is_some_and(|value| value != &row_base_head_version)
6325 {
6326 return Err(DbError::Message(
6327 "prepared Serial writes do not share one branch base".to_string(),
6328 ));
6329 }
6330 branch_id.get_or_insert(row_branch_id);
6331 base.get_or_insert(row_base);
6332 base_head_bytes.get_or_insert(row_base_head_bytes);
6333 base_head_version.get_or_insert(row_base_head_version);
6334 let unverified: StoreBatchCommit =
6335 serde_json::from_slice(&commit.semantic_bytes).map_err(|error| {
6336 DbError::Message(format!("prepared Serial commit: {error}"))
6337 })?;
6338 if unverified.write_id.as_str() != write_id {
6339 return Err(DbError::Message(
6340 "prepared Serial write id differs from signed commit".to_string(),
6341 ));
6342 }
6343 if writes.is_empty() {
6344 predecessor = base.as_ref().expect("first row stored base").clone();
6345 }
6346 let expected_sequence = predecessor
6347 .as_ref()
6348 .map_or(1, |reference| reference.coord.sequence().saturating_add(1));
6349 let coord = StoreCommitCoord::Serial {
6350 sequence: expected_sequence,
6351 };
6352 let registration = load_activated_registration_on(
6353 conn,
6354 &root,
6355 &unverified.author_registration,
6356 )?;
6357 let commit_value = StoreBatchCommit::parse_at(
6358 &commit.semantic_bytes,
6359 root.store_root_hash,
6360 &coord,
6361 ®istration,
6362 )
6363 .map_err(|error| {
6364 DbError::Message(format!("verify prepared Serial commit: {error}"))
6365 })?;
6366 let order_matches = match (&predecessor, &commit_value.order) {
6367 (
6368 Some(expected),
6369 crate::sync::store_commit::StoreCommitOrder::Serial {
6370 predecessor: StoreSerialPredecessor::Commit(actual),
6371 ..
6372 },
6373 ) => actual == expected,
6374 (
6375 None,
6376 crate::sync::store_commit::StoreCommitOrder::Serial {
6377 predecessor:
6378 StoreSerialPredecessor::Genesis {
6379 root: commit_root,
6380 founder_registration,
6381 },
6382 ..
6383 },
6384 ) => {
6385 commit_root == &root
6386 && founder_registration == &commit_value.author_registration
6387 }
6388 _ => false,
6389 };
6390 if !order_matches {
6391 return Err(DbError::Message(
6392 "prepared Serial commit chain has a different exact predecessor"
6393 .to_string(),
6394 ));
6395 }
6396 let commit_ref = StoreBatchCommitRef::from_commit(
6397 &commit_value,
6398 coord,
6399 commit.prepared.reference().clone(),
6400 )
6401 .map_err(|error| DbError::Message(error.to_string()))?;
6402 let write_id = WriteId::from_generated(write_id);
6403 let partitions = Self::prepared_store_write_partitions_on(
6404 conn,
6405 write_id.as_str(),
6406 &stored_changeset,
6407 WritePolicy::Serial,
6408 )?;
6409 let audiences = load_prepared_audience_objects_on(conn, &write_id)?;
6410 Self::validate_loaded_write_objects(
6411 &write_id,
6412 &commit_ref,
6413 &commit_value,
6414 &partitions,
6415 &audiences,
6416 )?;
6417 if let Some(head_bytes) = tip_head_bytes {
6418 if head.is_some() {
6419 return Err(DbError::Message(
6420 "prepared Serial branch has more than one tip head".to_string(),
6421 ));
6422 }
6423 let value = StoreSerialHead::parse(
6424 &head_bytes,
6425 root.store_root_hash,
6426 ®istration,
6427 )
6428 .map_err(|error| {
6429 DbError::Message(format!("verify prepared Serial head: {error}"))
6430 })?;
6431 head = Some(CanonicalProtocolObject {
6432 value,
6433 bytes: head_bytes,
6434 });
6435 }
6436 writes.push(PreparedSerialStoreWriteCommit {
6437 audiences,
6438 commit: ExactProtocolObject {
6439 value: commit_value,
6440 bytes: commit.semantic_bytes,
6441 object: commit.prepared.reference().clone(),
6442 prepared: commit.prepared,
6443 },
6444 });
6445 predecessor = Some(commit_ref);
6446 }
6447 if writes.is_empty() {
6448 return Ok(None);
6449 }
6450 let head = head.ok_or_else(|| {
6451 DbError::Message(
6452 "prepared Serial branch has no activating tip head".to_string(),
6453 )
6454 })?;
6455 let tip_ref = predecessor.expect("nonempty branch has exact tip");
6456 let tip_author = &writes
6457 .last()
6458 .expect("nonempty branch")
6459 .commit
6460 .value
6461 .author_registration;
6462 if !matches!(
6463 &head.value.state,
6464 StoreSerialHeadState::Commit {
6465 author_registration,
6466 commit,
6467 } if author_registration == tip_author && commit == &tip_ref
6468 ) {
6469 return Err(DbError::Message(
6470 "prepared Serial head does not activate the exact final commit".to_string(),
6471 ));
6472 }
6473 let base_value = base.expect("nonempty branch");
6474 let base_bytes = base_head_bytes.expect("nonempty branch");
6475 let base_version = base_head_version.expect("nonempty branch");
6476 if base_bytes.is_some() != base_version.is_some() {
6477 return Err(DbError::Message(
6478 "Serial base head bytes and opaque version receipt differ in presence"
6479 .to_string(),
6480 ));
6481 }
6482 match base_bytes.as_deref() {
6483 Some(bytes) => {
6484 let unverified: StoreSerialHead =
6485 serde_json::from_slice(bytes).map_err(|error| {
6486 DbError::Message(format!("stored Serial base head: {error}"))
6487 })?;
6488 let executor_ref = match &unverified.state {
6489 StoreSerialHeadState::Genesis {
6490 founder_registration,
6491 ..
6492 } => founder_registration,
6493 StoreSerialHeadState::Commit {
6494 author_registration,
6495 ..
6496 } => author_registration,
6497 };
6498 let executor = load_activated_registration_on(conn, &root, executor_ref)?;
6499 let verified =
6500 StoreSerialHead::parse(bytes, root.store_root_hash, &executor)
6501 .map_err(|error| {
6502 DbError::Message(format!(
6503 "verify stored Serial base head: {error}"
6504 ))
6505 })?;
6506 let observed = match verified.state {
6507 StoreSerialHeadState::Genesis {
6508 root: observed_root,
6509 ..
6510 } => {
6511 if observed_root != root {
6512 return Err(DbError::Message(
6513 "stored Serial genesis head has a different exact root"
6514 .to_string(),
6515 ));
6516 }
6517 None
6518 }
6519 StoreSerialHeadState::Commit { commit, .. } => Some(commit),
6520 };
6521 if observed != base_value {
6522 return Err(DbError::Message(
6523 "stored Serial base evidence differs from branch base".to_string(),
6524 ));
6525 }
6526 }
6527 None if base_value.is_some() => {
6528 return Err(DbError::Message(
6529 "stored Serial base commit has no head evidence".to_string(),
6530 ));
6531 }
6532 None => {}
6533 }
6534 Ok(Some(PreparedSerialStoreBranch {
6535 branch_id: branch_id.expect("nonempty branch"),
6536 base: base_value,
6537 base_head_bytes: base_bytes,
6538 base_head_version: base_version,
6539 writes,
6540 head,
6541 }))
6542 })
6543 .await?;
6544 if let Some(branch) = &loaded {
6545 for write in &branch.writes {
6546 for blob in &write.audiences.blobs {
6547 if let Some(spool_path) = blob.spool_path() {
6548 crate::local_blob::verify_exact_file(blob.blob().object(), spool_path)
6549 .await
6550 .map_err(|error| {
6551 DbError::Message(format!("prepared Serial blob spool: {error}"))
6552 })?;
6553 }
6554 }
6555 }
6556 }
6557 Ok(loaded)
6558 }
6559
6560 pub(crate) async fn prepared_serial_candidate_abandonment(
6561 &self,
6562 ) -> Result<Option<PreparedSerialCandidateAbandonment>, DbError> {
6563 self.call(|conn| {
6564 let raw: Option<String> = conn
6565 .query_row(
6566 "SELECT value FROM protocol_state WHERE key = ?1",
6567 [SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY],
6568 |row| row.get(0),
6569 )
6570 .optional()
6571 .map_err(DbError::from)?;
6572 let Some(raw) = raw else {
6573 return Ok(None);
6574 };
6575 let durable: DurableSerialCandidateAbandonment =
6576 serde_json::from_str(&raw).map_err(|error| {
6577 DbError::Message(format!("prepared Serial candidate abandonment: {error}"))
6578 })?;
6579 let expected_base = serde_json::to_string(&StoreWriteBase::Serial {
6580 branch_id: durable.branch_id.clone(),
6581 base: durable.base.clone(),
6582 })
6583 .map_err(|error| {
6584 DbError::Message(format!("Serial abandonment branch base: {error}"))
6585 })?;
6586 let raw_prepared: String = conn
6587 .query_row(
6588 "SELECT prepared FROM store_writes
6589 WHERE base = ?1 AND prepared IS NOT NULL
6590 ORDER BY ordinal LIMIT 1",
6591 [expected_base.as_str()],
6592 |row| row.get(0),
6593 )
6594 .map_err(DbError::from)?;
6595 let parsed = parse_prepared_serial_candidate(&raw_prepared)?.ok_or_else(|| {
6596 DbError::Message("Serial abandonment target is not an exact candidate".to_string())
6597 })?;
6598 if parsed.reference != durable.candidate {
6599 return Err(DbError::Message(
6600 "Serial abandonment target differs from its durable candidate".to_string(),
6601 ));
6602 }
6603 let prepared: PreparedStoreWriteState =
6604 serde_json::from_str(&raw_prepared).map_err(|error| {
6605 DbError::Message(format!("Serial abandonment candidate state: {error}"))
6606 })?;
6607 let PreparedStoreWriteState::Serial {
6608 base_head_bytes,
6609 base_head_version,
6610 commit,
6611 ..
6612 } = prepared
6613 else {
6614 return Err(DbError::Message(
6615 "Serial abandonment target is not an exact candidate".to_string(),
6616 ));
6617 };
6618 if base_head_bytes.as_deref() != Some(durable.base_head.bytes.as_slice())
6619 || base_head_version.as_ref() != Some(&durable.base_head.version)
6620 {
6621 return Err(DbError::Message(
6622 "Serial abandonment differs from its durable branch base".to_string(),
6623 ));
6624 }
6625 let candidate = ExactProtocolObject {
6626 value: parsed.commit,
6627 bytes: parsed.canonical_signed_bytes,
6628 object: parsed.reference.object,
6629 prepared: commit.prepared,
6630 };
6631 let raw_tip_prepared: String = conn
6632 .query_row(
6633 "SELECT prepared FROM store_writes
6634 WHERE base = ?1 AND prepared IS NOT NULL
6635 ORDER BY ordinal DESC LIMIT 1",
6636 [expected_base],
6637 |row| row.get(0),
6638 )
6639 .map_err(DbError::from)?;
6640 let tip_prepared: PreparedStoreWriteState = serde_json::from_str(&raw_tip_prepared)
6641 .map_err(|error| {
6642 DbError::Message(format!("Serial abandonment tip state: {error}"))
6643 })?;
6644 if !matches!(
6645 tip_prepared,
6646 PreparedStoreWriteState::Serial {
6647 tip_head_bytes: Some(ref tip_head_bytes),
6648 ..
6649 } if tip_head_bytes == &durable.original_head_bytes
6650 ) {
6651 return Err(DbError::Message(
6652 "Serial abandonment original head differs from its durable branch tip"
6653 .to_string(),
6654 ));
6655 }
6656 let root = required_store_root_authority_on(conn)?;
6657 let unverified: StoreBatchCommit =
6658 serde_json::from_slice(&durable.commit.semantic_bytes).map_err(|error| {
6659 DbError::Message(format!("Serial abandonment commit: {error}"))
6660 })?;
6661 let registration =
6662 load_activated_registration_on(conn, &root, &unverified.author_registration)?;
6663 let value = StoreBatchCommit::parse_at(
6664 &durable.commit.semantic_bytes,
6665 root.store_root_hash,
6666 &durable.candidate.coord,
6667 ®istration,
6668 )
6669 .map_err(|error| {
6670 DbError::Message(format!("verify Serial abandonment commit: {error}"))
6671 })?;
6672 let reference = StoreBatchCommitRef::from_commit(
6673 &value,
6674 durable.candidate.coord.clone(),
6675 durable.commit.prepared.reference().clone(),
6676 )
6677 .map_err(|error| DbError::Message(error.to_string()))?;
6678 if value.abandoned_candidates()
6679 != [crate::sync::store_commit::CandidateCleanupManifest {
6680 candidate: crate::sync::store_commit::StoreBatchCommitDeletionTarget {
6681 coord: durable.candidate.coord.clone(),
6682 object: durable.candidate.object.clone(),
6683 canonical_signed_bytes: candidate.bytes.clone(),
6684 },
6685 }]
6686 {
6687 return Err(DbError::Message(
6688 "durable Serial abandonment names another candidate".to_string(),
6689 ));
6690 }
6691 let head =
6692 StoreSerialHead::parse(&durable.head_bytes, root.store_root_hash, ®istration)
6693 .map_err(|error| {
6694 DbError::Message(format!("verify Serial abandonment head: {error}"))
6695 })?;
6696 if !matches!(
6697 &head.state,
6698 StoreSerialHeadState::Commit { commit, .. } if commit == &reference
6699 ) {
6700 return Err(DbError::Message(
6701 "durable Serial abandonment head names another commit".to_string(),
6702 ));
6703 }
6704 let unverified_original: StoreSerialHead =
6705 serde_json::from_slice(&durable.original_head_bytes).map_err(|error| {
6706 DbError::Message(format!("Serial abandonment original head: {error}"))
6707 })?;
6708 let original_author = match &unverified_original.state {
6709 StoreSerialHeadState::Commit {
6710 author_registration,
6711 ..
6712 } => author_registration,
6713 StoreSerialHeadState::Genesis { .. } => {
6714 return Err(DbError::Message(
6715 "Serial abandonment original head is not a commit".to_string(),
6716 ));
6717 }
6718 };
6719 let original_registration =
6720 load_activated_registration_on(conn, &root, original_author)?;
6721 StoreSerialHead::parse(
6722 &durable.original_head_bytes,
6723 root.store_root_hash,
6724 &original_registration,
6725 )
6726 .map_err(|error| {
6727 DbError::Message(format!("verify Serial abandonment original head: {error}"))
6728 })?;
6729 Ok(Some(PreparedSerialCandidateAbandonment {
6730 branch_id: durable.branch_id,
6731 base: durable.base,
6732 base_head: durable.base_head,
6733 authority: ExactProtocolObject {
6734 value,
6735 bytes: durable.commit.semantic_bytes,
6736 object: reference.object,
6737 prepared: durable.commit.prepared,
6738 },
6739 head: CanonicalProtocolObject {
6740 value: head,
6741 bytes: durable.head_bytes,
6742 },
6743 original_head_bytes: durable.original_head_bytes,
6744 durable_state: raw,
6745 }))
6746 })
6747 .await
6748 }
6749
6750 pub(crate) async fn latest_local_store_position(
6751 &self,
6752 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
6753 let write_policy = self.write_policy();
6754 self.call(move |conn| {
6755 let stream_id = match write_policy {
6756 WritePolicy::MergeConcurrent => {
6757 let (root, registration, _) = local_store_authority_on(conn)?;
6758 AuthorStreamId::store_announcements(&root, ®istration).to_string()
6759 }
6760 WritePolicy::Serial => SERIAL_STREAM_ID.to_string(),
6761 };
6762 Self::latest_position_for_device_on(conn, &stream_id)
6763 })
6764 .await
6765 }
6766
6767 pub(crate) async fn complete_prepared_store_write(
6768 &self,
6769 accepted: StoreBatchCommitRef,
6770 ) -> Result<(), DbError> {
6771 let statuses = self.state.write_statuses.clone();
6772 let gates = self.state.gates.clone();
6773 let synced_tables = self.state.synced_tables.clone();
6774 self.call(move |conn| {
6775 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
6776 let local_device_id: String = tx
6777 .query_row(
6778 "SELECT value FROM protocol_state WHERE key = ?1",
6779 [LOCAL_DEVICE_ID_STATE_KEY],
6780 |row| row.get(0),
6781 )
6782 .map_err(DbError::from)?;
6783 let prepared_count: i64 = tx
6784 .query_row(
6785 "SELECT COUNT(*) FROM store_writes WHERE prepared IS NOT NULL",
6786 [],
6787 |row| row.get(0),
6788 )
6789 .map_err(DbError::from)?;
6790 if prepared_count != 1 {
6791 return Err(DbError::Message(format!(
6792 "Store publication expected one prepared write, found {prepared_count}"
6793 )));
6794 }
6795 let (stored_write_id, raw_prepared): (String, String) = tx
6796 .query_row(
6797 "SELECT write_id, prepared FROM store_writes
6798 WHERE prepared IS NOT NULL ORDER BY ordinal LIMIT 1",
6799 [],
6800 |row| Ok((row.get(0)?, row.get(1)?)),
6801 )
6802 .map_err(DbError::from)?;
6803 let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
6804 .map_err(|error| DbError::Message(format!("prepared Store write: {error}")))?;
6805 if let PreparedStoreWriteState::MergeAbandonment {
6806 candidate_commit,
6807 candidate_head,
6808 authority_commit,
6809 authority_head,
6810 ..
6811 } = &prepared
6812 {
6813 let root = required_store_root_authority_on(&tx)?;
6814 let candidate =
6815 parse_prepared_merge_candidate_parts_on(&tx, candidate_commit, candidate_head)?;
6816 let authority =
6817 parse_prepared_merge_candidate_parts_on(&tx, authority_commit, authority_head)?;
6818 if authority.commit.write_id.as_str() != stored_write_id
6819 || accepted != authority.reference
6820 || !matches!(
6821 &authority.commit.body,
6822 crate::sync::store_commit::StoreCommitBody::AbandonCandidates { .. }
6823 )
6824 {
6825 return Err(DbError::Message(
6826 "accepted Merge abandonment differs from its durable authority".to_string(),
6827 ));
6828 }
6829 let registration = load_activated_registration_on(
6830 &tx,
6831 &root,
6832 &authority.commit.author_registration,
6833 )?;
6834 StoreDeviceHead::parse_at(
6835 &authority.head.to_bytes(),
6836 root.store_root_hash,
6837 ®istration,
6838 &accepted,
6839 )
6840 .map_err(|error| {
6841 DbError::Message(format!("verify accepted Merge abandonment head: {error}"))
6842 })?;
6843 for object in [
6844 authority_commit.prepared.reference(),
6845 authority_head.prepared.reference(),
6846 ] {
6847 let object_id = remote_object_id(object);
6848 let remote = load_remote_object_on(&tx, object_id)?
6849 .into_activated(&accepted)
6850 .map_err(|error| {
6851 DbError::Message(format!(
6852 "activate Merge abandonment object {object_id}: {error}"
6853 ))
6854 })?;
6855 update_remote_object_on(&tx, object_id, &remote)?;
6856 }
6857 let winner_head = crate::sync::store_commit::StoreDeviceHeadRef {
6858 head_hash: authority.head.head_hash(),
6859 object: authority.head_prepared.reference().clone(),
6860 };
6861 begin_merge_candidate_nonactivation_on(
6862 &tx,
6863 &WriteId::from_generated(stored_write_id.clone()),
6864 &candidate,
6865 winner_head.clone(),
6866 true,
6867 )?;
6868 Self::record_materialized_commit_on(&tx, &authority.commit, &accepted)?;
6869 let mut completed_preparation = prepared.clone();
6870 let PreparedStoreWriteState::MergeAbandonment { outcome, .. } =
6871 &mut completed_preparation
6872 else {
6873 unreachable!("matched Merge abandonment")
6874 };
6875 *outcome = MergeAbandonmentOutcome::Accepted {
6876 authority: accepted.clone(),
6877 };
6878 let completed_preparation =
6879 serde_json::to_string(&completed_preparation).map_err(|error| {
6880 DbError::Message(format!("serialize accepted Merge abandonment: {error}"))
6881 })?;
6882 let updated = tx
6883 .execute(
6884 "UPDATE store_writes SET prepared = ?2
6885 WHERE write_id = ?1 AND prepared = ?3",
6886 rusqlite::params![
6887 stored_write_id.as_str(),
6888 completed_preparation,
6889 raw_prepared
6890 ],
6891 )
6892 .map_err(DbError::from)?;
6893 if updated != 1 {
6894 return Err(DbError::Message(
6895 "Merge abandonment changed during activation".to_string(),
6896 ));
6897 }
6898 let blocked = WriteStatus::Blocked(crate::WriteBlock::InvalidProtocolState {
6899 reason: format!(
6900 "candidate abandonment {} is accepted; exact cleanup is pending",
6901 winner_head.head_hash
6902 ),
6903 });
6904 let write_id = authority.commit.write_id.clone();
6905 Self::set_write_status_on(&tx, &write_id, &blocked)?;
6906 tx.commit().map_err(DbError::from)?;
6907 Self::notify_write_status_in(&statuses, &write_id, blocked);
6908 return Ok(());
6909 }
6910 let PreparedStoreWriteState::MergeConcurrent {
6911 commit,
6912 head,
6913 local_cleanup,
6914 ..
6915 } = prepared
6916 else {
6917 return Err(DbError::Message(
6918 "serial branch reached MergeConcurrent completion".to_string(),
6919 ));
6920 };
6921 let root = required_store_root_authority_on(&tx)?;
6922 let unverified: StoreBatchCommit = serde_json::from_slice(&commit.semantic_bytes)
6923 .map_err(|error| DbError::Message(format!("prepared Store commit: {error}")))?;
6924 let registration =
6925 load_activated_registration_on(&tx, &root, &unverified.author_registration)?;
6926 let expected_stream =
6927 AuthorStreamId::store_announcements(&root, &unverified.author_registration);
6928 if !matches!(
6929 accepted.coord,
6930 StoreCommitCoord::MergeConcurrent { stream_id, .. }
6931 if stream_id == expected_stream
6932 ) || accepted.object != *commit.prepared.reference()
6933 {
6934 return Err(DbError::Message(
6935 "accepted Merge head differs from the exact prepared commit".to_string(),
6936 ));
6937 }
6938 let commit_value = StoreBatchCommit::parse_at(
6939 &commit.semantic_bytes,
6940 root.store_root_hash,
6941 &accepted.coord,
6942 ®istration,
6943 )
6944 .map_err(|error| DbError::Message(format!("outbound commit: {error}")))?;
6945 accepted
6946 .verify_commit(&commit_value)
6947 .map_err(|error| DbError::Message(error.to_string()))?;
6948 if commit_value.write_id.as_str() != stored_write_id {
6949 return Err(DbError::Message(
6950 "prepared write id differs from signed commit".to_string(),
6951 ));
6952 }
6953 let write_id = commit_value.write_id.clone();
6954 let head_object_id = remote_object_id(head.prepared.reference());
6955 Self::activate_prepared_write_on(
6956 &tx,
6957 &gates,
6958 &synced_tables,
6959 &write_id,
6960 &commit_value,
6961 &accepted,
6962 local_cleanup,
6963 &[head_object_id],
6964 )?;
6965 let cleared = tx
6966 .execute(
6967 "UPDATE store_writes SET prepared = NULL
6968 WHERE write_id = ?1 AND prepared IS NOT NULL",
6969 [stored_write_id.as_str()],
6970 )
6971 .map_err(DbError::from)?;
6972 if cleared != 1 {
6973 return Err(DbError::Message(
6974 "prepared Store write disappeared".to_string(),
6975 ));
6976 }
6977 let status = WriteStatus::Published(Box::new(PublishedPosition::MergeConcurrent {
6978 device_id: local_device_id,
6979 commit: accepted.clone(),
6980 }));
6981 Self::set_write_status_on(&tx, &write_id, &status)?;
6982 tx.commit().map_err(DbError::from)?;
6983 Self::notify_write_status_in(&statuses, &write_id, status);
6984 Ok(())
6985 })
6986 .await
6987 }
6988
6989 pub(crate) async fn mark_serial_branch_conflict(
6990 &self,
6991 branch_id: PendingBranchId,
6992 base: Option<StoreBatchCommitRef>,
6993 current: StoreSerialPredecessor,
6994 ) -> Result<(), DbError> {
6995 let statuses = self.state.write_statuses.clone();
6996 self.call(move |conn| {
6997 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
6998 let expected_base = StoreWriteBase::Serial {
6999 branch_id: branch_id.clone(),
7000 base: base.clone(),
7001 };
7002 let conflict = crate::SerializationConflict {
7003 branch_id: branch_id.clone(),
7004 base: store_serial_predecessor_on(&tx, base.as_ref())?,
7005 current,
7006 };
7007 let status = WriteStatus::Conflict(Box::new(conflict));
7008 let status_json = serde_json::to_string(&status)
7009 .map_err(|error| DbError::Message(format!("serialize Serial conflict: {error}")))?;
7010 let mut statement = tx
7011 .prepare(
7012 "SELECT write_id, base FROM store_writes
7013 WHERE status != '\"local_only\"'
7014 AND json_extract(status, '$.published') IS NULL
7015 AND json_extract(status, '$.resolved') IS NULL
7016 ORDER BY ordinal",
7017 )
7018 .map_err(DbError::from)?;
7019 let rows = statement
7020 .query_map([], |row| {
7021 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
7022 })
7023 .map_err(DbError::from)?;
7024 let write_ids = Self::write_ids_matching_serial_base(rows, &expected_base)?;
7025 drop(statement);
7026 if write_ids.is_empty() {
7027 return Err(DbError::Message(format!(
7028 "Serial branch {:?} has no pending writes",
7029 branch_id.first_write_id()
7030 )));
7031 }
7032 for write_id in &write_ids {
7033 let updated = tx
7034 .execute(
7035 "UPDATE store_writes SET status = ?2 WHERE write_id = ?1",
7036 rusqlite::params![write_id.as_str(), &status_json],
7037 )
7038 .map_err(DbError::from)?;
7039 if updated != 1 {
7040 return Err(DbError::Message(format!(
7041 "Serial conflict write {write_id} disappeared"
7042 )));
7043 }
7044 }
7045 tx.commit().map_err(DbError::from)?;
7046 for write_id in write_ids {
7047 Self::notify_write_status_in(&statuses, &write_id, status.clone());
7048 }
7049 Ok(())
7050 })
7051 .await
7052 }
7053
7054 pub(crate) async fn mark_merge_candidate_conflict(
7055 &self,
7056 write_id: WriteId,
7057 winner_commit: StoreBatchCommitRef,
7058 winner_head: crate::sync::store_commit::StoreDeviceHeadRef,
7059 ) -> Result<(), DbError> {
7060 let statuses = self.state.write_statuses.clone();
7061 let notified_write_id = write_id.clone();
7062 self.call(move |conn| {
7063 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
7064 let (raw_status, raw_prepared): (String, String) = tx
7065 .query_row(
7066 "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
7067 [write_id.as_str()],
7068 |row| Ok((row.get(0)?, row.get(1)?)),
7069 )
7070 .map_err(DbError::from)?;
7071 let status: WriteStatus = serde_json::from_str(&raw_status)
7072 .map_err(|error| DbError::Message(format!("Merge candidate status: {error}")))?;
7073 if !matches!(status, WriteStatus::Publishing) {
7074 return Err(DbError::Message(format!(
7075 "Merge candidate {write_id} is not publishing"
7076 )));
7077 }
7078 let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
7079 .map_err(|error| DbError::Message(format!("prepared Merge candidate: {error}")))?;
7080 let prepared_candidate = parse_prepared_merge_candidate_on(&tx, &prepared)?
7081 .ok_or_else(|| {
7082 DbError::Message("Serial branch reached Merge candidate conflict".to_string())
7083 })?;
7084 let publication = parse_prepared_merge_publication_on(&tx, &prepared)?
7085 .expect("parsed Merge publication");
7086 if winner_head.object.slot() != publication.head_prepared.reference().slot()
7087 || winner_head.object == *publication.head_prepared.reference()
7088 {
7089 return Err(DbError::Message(
7090 "Merge winner does not replace the prepared exact head slot".to_string(),
7091 ));
7092 }
7093 if prepared_candidate.commit.write_id != write_id {
7094 return Err(DbError::Message(
7095 "prepared Merge graph differs from its write identity".to_string(),
7096 ));
7097 }
7098 if matches!(&prepared, PreparedStoreWriteState::MergeAbandonment { .. }) {
7099 begin_merge_candidate_nonactivation_on(
7100 &tx,
7101 &write_id,
7102 &publication,
7103 winner_head.clone(),
7104 false,
7105 )?;
7106 if winner_commit != prepared_candidate.reference {
7107 begin_merge_candidate_nonactivation_on(
7108 &tx,
7109 &write_id,
7110 &prepared_candidate,
7111 winner_head.clone(),
7112 true,
7113 )?;
7114 }
7115 let mut lost_preparation = prepared.clone();
7116 let PreparedStoreWriteState::MergeAbandonment { outcome, .. } =
7117 &mut lost_preparation
7118 else {
7119 unreachable!("matched Merge abandonment")
7120 };
7121 *outcome = MergeAbandonmentOutcome::Lost {
7122 winner_commit: winner_commit.clone(),
7123 winner_head: winner_head.clone(),
7124 };
7125 let lost_preparation =
7126 serde_json::to_string(&lost_preparation).map_err(|error| {
7127 DbError::Message(format!("serialize lost Merge abandonment: {error}"))
7128 })?;
7129 let updated = tx
7130 .execute(
7131 "UPDATE store_writes SET prepared = ?2
7132 WHERE write_id = ?1 AND prepared = ?3",
7133 rusqlite::params![write_id.as_str(), lost_preparation, raw_prepared],
7134 )
7135 .map_err(DbError::from)?;
7136 if updated != 1 {
7137 return Err(DbError::Message(
7138 "Merge abandonment changed while recording its winner".to_string(),
7139 ));
7140 }
7141 } else {
7142 begin_merge_candidate_nonactivation_on(
7143 &tx,
7144 &write_id,
7145 &prepared_candidate,
7146 winner_head.clone(),
7147 true,
7148 )?;
7149 }
7150 let blocked = WriteStatus::Blocked(crate::WriteBlock::InvalidProtocolState {
7151 reason: format!(
7152 "Merge successor slot is occupied by signed head {}",
7153 winner_head.head_hash
7154 ),
7155 });
7156 Self::set_write_status_on(&tx, &write_id, &blocked)?;
7157 tx.commit().map_err(DbError::from)?;
7158 Self::notify_write_status_in(&statuses, ¬ified_write_id, blocked);
7159 Ok(())
7160 })
7161 .await
7162 }
7163
7164 pub(crate) async fn exact_serial_predecessor(
7165 &self,
7166 commit: Option<StoreBatchCommitRef>,
7167 ) -> Result<StoreSerialPredecessor, DbError> {
7168 self.call(move |conn| store_serial_predecessor_on(conn, commit.as_ref()))
7169 .await
7170 }
7171
7172 pub(crate) async fn complete_prepared_serial_branch(
7173 &self,
7174 accepted: VersionedObject,
7175 ) -> Result<u64, DbError> {
7176 let statuses = self.state.write_statuses.clone();
7177 let gates = self.state.gates.clone();
7178 let synced_tables = self.state.synced_tables.clone();
7179 self.call(move |conn| {
7180 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
7181 let root = required_store_root_authority_on(&tx)?;
7182 let accepted_unverified: StoreSerialHead = serde_json::from_slice(&accepted.bytes)
7183 .map_err(|error| DbError::Message(format!("accepted Serial head: {error}")))?;
7184 let accepted_author_ref = match &accepted_unverified.state {
7185 StoreSerialHeadState::Commit {
7186 author_registration,
7187 ..
7188 } => author_registration,
7189 StoreSerialHeadState::Genesis { .. } => {
7190 return Err(DbError::Message(
7191 "prepared Serial branch cannot complete at a genesis head".to_string(),
7192 ));
7193 }
7194 };
7195 let accepted_author = load_activated_registration_on(&tx, &root, accepted_author_ref)?;
7196 let accepted_head =
7197 StoreSerialHead::parse(&accepted.bytes, root.store_root_hash, &accepted_author)
7198 .map_err(|error| {
7199 DbError::Message(format!("verify accepted Serial head: {error}"))
7200 })?;
7201 let accepted_tip = match &accepted_head.state {
7202 StoreSerialHeadState::Commit { commit, .. } => commit.clone(),
7203 StoreSerialHeadState::Genesis { .. } => unreachable!("rejected above"),
7204 };
7205 let mut statement = tx
7206 .prepare(
7207 "SELECT write_id, prepared, base FROM store_writes
7208 WHERE prepared IS NOT NULL AND status = '\"publishing\"'
7209 ORDER BY ordinal",
7210 )
7211 .map_err(DbError::from)?;
7212 let rows = statement
7213 .query_map([], |row| {
7214 Ok((
7215 row.get::<_, String>(0)?,
7216 row.get::<_, String>(1)?,
7217 row.get::<_, String>(2)?,
7218 ))
7219 })
7220 .map_err(DbError::from)?
7221 .collect::<Result<Vec<_>, _>>()
7222 .map_err(DbError::from)?;
7223 drop(statement);
7224 let mut completed = Vec::new();
7225 let mut completed_base = None;
7226 let mut predecessor = None;
7227 let mut prepared_tip_head = None;
7228 for row in rows {
7229 let (stored_write_id, prepared, raw_base) = row;
7230 let stored_base: StoreWriteBase = serde_json::from_str(&raw_base)
7231 .map_err(|error| DbError::Message(format!("prepared Serial base: {error}")))?;
7232 match &completed_base {
7233 Some(expected) if expected != &stored_base => {
7234 return Err(DbError::Message(
7235 "prepared Serial branch contains inconsistent bases".to_string(),
7236 ));
7237 }
7238 None => completed_base = Some(stored_base.clone()),
7239 Some(_) => {}
7240 }
7241 let PreparedStoreWriteState::Serial {
7242 commit,
7243 tip_head_bytes,
7244 local_cleanup,
7245 ..
7246 } = serde_json::from_str(&prepared)
7247 .map_err(|error| DbError::Message(format!("prepared Serial write: {error}")))?
7248 else {
7249 return Err(DbError::Message(
7250 "non-Serial write reached Serial completion".to_string(),
7251 ));
7252 };
7253 let unverified: StoreBatchCommit = serde_json::from_slice(&commit.semantic_bytes)
7254 .map_err(|error| {
7255 DbError::Message(format!("prepared Serial commit: {error}"))
7256 })?;
7257 if predecessor.is_none() {
7258 let StoreWriteBase::Serial { base, .. } = &stored_base else {
7259 return Err(DbError::Message(
7260 "Merge base reached Serial completion".to_string(),
7261 ));
7262 };
7263 predecessor = base.clone();
7264 }
7265 let sequence = predecessor
7266 .as_ref()
7267 .map_or(1, |reference: &StoreBatchCommitRef| {
7268 reference.coord.sequence().saturating_add(1)
7269 });
7270 let coord = StoreCommitCoord::Serial { sequence };
7271 let registration =
7272 load_activated_registration_on(&tx, &root, &unverified.author_registration)?;
7273 let commit_value = StoreBatchCommit::parse_at(
7274 &commit.semantic_bytes,
7275 root.store_root_hash,
7276 &coord,
7277 ®istration,
7278 )
7279 .map_err(|error| DbError::Message(format!("outbound Serial commit: {error}")))?;
7280 let commit_ref = StoreBatchCommitRef::from_commit(
7281 &commit_value,
7282 coord,
7283 commit.prepared.reference().clone(),
7284 )
7285 .map_err(|error| DbError::Message(error.to_string()))?;
7286 let order_matches = match (&predecessor, &commit_value.order) {
7287 (
7288 Some(expected),
7289 crate::sync::store_commit::StoreCommitOrder::Serial {
7290 predecessor: StoreSerialPredecessor::Commit(actual),
7291 ..
7292 },
7293 ) => actual == expected,
7294 (
7295 None,
7296 crate::sync::store_commit::StoreCommitOrder::Serial {
7297 predecessor:
7298 StoreSerialPredecessor::Genesis {
7299 root: commit_root,
7300 founder_registration,
7301 },
7302 ..
7303 },
7304 ) => {
7305 commit_root == &root
7306 && founder_registration == &commit_value.author_registration
7307 }
7308 _ => false,
7309 };
7310 if !order_matches {
7311 return Err(DbError::Message(
7312 "prepared Serial completion chain has a different exact predecessor"
7313 .to_string(),
7314 ));
7315 }
7316 if commit_value.write_id.as_str() != stored_write_id {
7317 return Err(DbError::Message(
7318 "prepared Serial write id differs from signed commit".to_string(),
7319 ));
7320 }
7321 let write_id = commit_value.write_id.clone();
7322 Self::activate_prepared_write_on(
7323 &tx,
7324 &gates,
7325 &synced_tables,
7326 &write_id,
7327 &commit_value,
7328 &commit_ref,
7329 local_cleanup,
7330 &[],
7331 )?;
7332 let cleared = tx
7333 .execute(
7334 "UPDATE store_writes SET prepared = NULL WHERE write_id = ?1",
7335 [write_id.as_str()],
7336 )
7337 .map_err(DbError::from)?;
7338 if cleared != 1 {
7339 return Err(DbError::Message(format!(
7340 "prepared Serial write {write_id} disappeared during completion"
7341 )));
7342 }
7343 let status = WriteStatus::Published(Box::new(PublishedPosition::Serial {
7344 commit: commit_ref.clone(),
7345 }));
7346 Self::set_write_status_on(&tx, &write_id, &status)?;
7347 if let Some(bytes) = tip_head_bytes {
7348 if prepared_tip_head.replace(bytes).is_some() {
7349 return Err(DbError::Message(
7350 "prepared Serial branch has multiple tip heads".to_string(),
7351 ));
7352 }
7353 }
7354 predecessor = Some(commit_ref.clone());
7355 completed.push((write_id, status, commit_ref));
7356 }
7357 let Some((_, _, final_ref)) = completed.last() else {
7358 return Err(DbError::Message(
7359 "prepared Serial branch is absent".to_string(),
7360 ));
7361 };
7362 if final_ref != &accepted_tip
7363 || prepared_tip_head.as_deref() != Some(accepted.bytes.as_slice())
7364 {
7365 return Err(DbError::Message(
7366 "accepted Serial head differs from the exact prepared branch tip".to_string(),
7367 ));
7368 }
7369 tx.execute(
7370 "INSERT INTO serial_head_receipt \
7371 (singleton, head_bytes, version_token, commit_ref) VALUES (1, ?1, ?2, ?3) \
7372 ON CONFLICT(singleton) DO UPDATE SET \
7373 head_bytes = excluded.head_bytes, \
7374 version_token = excluded.version_token, \
7375 commit_ref = excluded.commit_ref",
7376 (
7377 &accepted.bytes,
7378 serde_json::to_string(&accepted.version).map_err(|error| {
7379 DbError::Message(format!("serialize Serial version receipt: {error}"))
7380 })?,
7381 serde_json::to_string(&accepted_tip).map_err(|error| {
7382 DbError::Message(format!("serialize accepted Serial commit ref: {error}"))
7383 })?,
7384 ),
7385 )
7386 .map_err(DbError::from)?;
7387 let completed_base = completed_base.ok_or_else(|| {
7388 DbError::Message("prepared Serial branch base is absent".to_string())
7389 })?;
7390 let suffix_first: Option<String> = tx
7391 .query_row(
7392 "SELECT write_id FROM store_writes
7393 WHERE status = '\"pending\"' AND prepared IS NULL AND base = ?1
7394 ORDER BY ordinal LIMIT 1",
7395 [serde_json::to_string(&completed_base).map_err(|error| {
7396 DbError::Message(format!("serialize Serial base: {error}"))
7397 })?],
7398 |row| row.get(0),
7399 )
7400 .optional()
7401 .map_err(DbError::from)?;
7402 if let Some(suffix_first) = suffix_first {
7403 let rebased = StoreWriteBase::Serial {
7404 branch_id: PendingBranchId::from_first_write(WriteId::from_generated(
7405 suffix_first,
7406 )),
7407 base: Some(accepted_tip.clone()),
7408 };
7409 tx.execute(
7410 "UPDATE store_writes SET base = ?2
7411 WHERE status = '\"pending\"' AND prepared IS NULL AND base = ?1",
7412 rusqlite::params![
7413 serde_json::to_string(&completed_base).map_err(
7414 |error| DbError::Message(format!(
7415 "serialize completed Serial base: {error}"
7416 ))
7417 )?,
7418 serde_json::to_string(&rebased).map_err(|error| DbError::Message(
7419 format!("serialize rebased Serial suffix: {error}")
7420 ))?,
7421 ],
7422 )
7423 .map_err(DbError::from)?;
7424 }
7425 let count = u64::try_from(completed.len())
7426 .map_err(|_| DbError::Message("Serial completion count exceeds u64".to_string()))?;
7427 tx.commit().map_err(DbError::from)?;
7428 for (write_id, status, _) in completed {
7429 Self::notify_write_status_in(&statuses, &write_id, status);
7430 }
7431 Ok(count)
7432 })
7433 .await
7434 }
7435
7436 fn validate_serial_candidate_cleanup_on(
7437 conn: &Connection,
7438 write_id: &WriteId,
7439 raw_prepared: Option<&str>,
7440 ) -> Result<(), DbError> {
7441 let Some(raw_prepared) = raw_prepared else {
7442 return Ok(());
7443 };
7444 let Some(candidate) = parse_prepared_serial_candidate(raw_prepared)? else {
7445 return Ok(());
7446 };
7447 let mut object_ids = vec![remote_object_id(&candidate.reference.object)];
7448 let mut statement = conn
7449 .prepare(
7450 "SELECT remote_object_id FROM store_write_packages WHERE write_id = ?1
7451 UNION
7452 SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1",
7453 )
7454 .map_err(DbError::from)?;
7455 let indexed = statement
7456 .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
7457 .map_err(DbError::from)?
7458 .collect::<Result<Vec<_>, _>>()
7459 .map_err(DbError::from)?;
7460 drop(statement);
7461 for encoded in indexed {
7462 object_ids.push(encoded.parse().map_err(|error| {
7463 DbError::Message(format!("Serial cleanup remote object id: {error}"))
7464 })?);
7465 }
7466 for object_id in object_ids {
7467 let remote = load_remote_object_on(conn, object_id)?;
7468 if !remote
7469 .candidate_cleanup_complete(&candidate.reference)
7470 .map_err(|error| {
7471 DbError::Message(format!(
7472 "validate candidate cleanup for {object_id}: {error}"
7473 ))
7474 })?
7475 {
7476 return Err(DbError::Message(format!(
7477 "candidate cleanup for remote object {object_id} is incomplete"
7478 )));
7479 }
7480 }
7481 Ok(())
7482 }
7483
7484 fn apply_serial_resolution_on(
7485 tx: &rusqlite::Transaction<'_>,
7486 synced_tables: &[SyncedTable],
7487 branch_id: &PendingBranchId,
7488 plan: crate::sync::store_pull::SerialResolutionPlan,
7489 ) -> Result<Vec<WriteId>, DbError> {
7490 let schema = Arc::new(crate::sync::conflict::TableSchema::from_db(
7491 tx,
7492 synced_tables,
7493 )?);
7494 let mut statement = tx
7495 .prepare(
7496 "SELECT write_id, status, inverse_changeset, base, prepared FROM store_writes
7497 WHERE status != '\"local_only\"'
7498 AND json_extract(status, '$.published') IS NULL
7499 AND json_extract(status, '$.resolved') IS NULL
7500 AND json_type(base, '$.serial') IS NOT NULL
7501 ORDER BY ordinal",
7502 )
7503 .map_err(DbError::from)?;
7504 let rows = statement
7505 .query_map([], |row| {
7506 Ok((
7507 row.get::<_, String>(0)?,
7508 row.get::<_, String>(1)?,
7509 row.get::<_, Vec<u8>>(2)?,
7510 row.get::<_, String>(3)?,
7511 row.get::<_, Option<String>>(4)?,
7512 ))
7513 })
7514 .map_err(DbError::from)?;
7515 let mut branch = Vec::new();
7516 let mut branch_base = None;
7517 let mut saw_conflict = false;
7518 for row in rows {
7519 let (write_id, status, inverse, raw_base, prepared) = row.map_err(DbError::from)?;
7520 let status: WriteStatus = serde_json::from_str(&status)
7521 .map_err(|error| DbError::Message(format!("Serial branch status: {error}")))?;
7522 let base: StoreWriteBase = serde_json::from_str(&raw_base)
7523 .map_err(|error| DbError::Message(format!("Serial branch base: {error}")))?;
7524 let StoreWriteBase::Serial {
7525 branch_id: stored_branch_id,
7526 base,
7527 } = base
7528 else {
7529 return Err(DbError::Message(
7530 "MergeConcurrent base reached Serial resolution".to_string(),
7531 ));
7532 };
7533 if &stored_branch_id != branch_id {
7534 return Err(DbError::Message(
7535 "Serial database contains more than one unresolved branch".to_string(),
7536 ));
7537 }
7538 match status {
7539 WriteStatus::Conflict(conflict) => {
7540 if conflict.branch_id != stored_branch_id
7541 || conflict.base != store_serial_predecessor_on(tx, base.as_ref())?
7542 {
7543 return Err(DbError::Message(
7544 "Serial conflict status differs from its durable branch base"
7545 .to_string(),
7546 ));
7547 }
7548 saw_conflict = true;
7549 }
7550 WriteStatus::Pending if prepared.is_none() => {}
7551 status => {
7552 return Err(DbError::Message(format!(
7553 "conflicted Serial branch write {write_id} has non-resolvable status {status:?}"
7554 )))
7555 }
7556 }
7557 Self::validate_serial_candidate_cleanup_on(
7558 tx,
7559 &WriteId::from_generated(write_id.clone()),
7560 prepared.as_deref(),
7561 )?;
7562 if branch_base.as_ref().is_some_and(|stored| stored != &base) {
7563 return Err(DbError::Message(
7564 "Serial conflict branch has inconsistent bases".to_string(),
7565 ));
7566 }
7567 branch_base.get_or_insert(base);
7568 branch.push((WriteId::from_generated(write_id), inverse));
7569 }
7570 drop(statement);
7571 if branch.is_empty() || !saw_conflict {
7572 return Err(DbError::Message(format!(
7573 "Serial branch {} is not conflicted",
7574 branch_id.first_write_id()
7575 )));
7576 }
7577 let branch_base = branch_base.expect("nonempty branch has a base value");
7578 let durable_base = Self::latest_position_for_device_on(tx, SERIAL_STREAM_ID)?;
7579 if durable_base != branch_base {
7580 return Err(DbError::Message(format!(
7581 "local Serial position {durable_base:?} differs from branch base {branch_base:?}"
7582 )));
7583 }
7584 for (_, inverse) in branch.iter().rev() {
7585 let inverse = crate::sync::apply::ValidatedChangeset::new(inverse, schema.clone())
7586 .map_err(|error| DbError::Message(format!("invalid Serial inverse: {error}")))?;
7587 crate::sync::apply::apply_changeset_strict_on(tx, inverse)
7588 .map_err(|error| DbError::Message(format!("reverse Serial branch: {error}")))?;
7589 }
7590 let mut predecessor = branch_base;
7591 for resolution in plan.commits {
7592 let expected_seq = match predecessor.as_ref() {
7593 Some(reference) => reference.coord.sequence().checked_add(1).ok_or_else(|| {
7594 DbError::Message("Serial resolution sequence overflow".to_string())
7595 })?,
7596 None => 1,
7597 };
7598 if resolution.commit.seq() != expected_seq
7599 || resolution.commit_ref.coord
7600 != (StoreCommitCoord::Serial {
7601 sequence: expected_seq,
7602 })
7603 || resolution.commit.order.predecessor() != predecessor.as_ref()
7604 {
7605 return Err(DbError::Message(format!(
7606 "Serial resolution commit {} does not follow the branch base",
7607 resolution.commit.seq()
7608 )));
7609 }
7610 if let Some(package) = resolution.package {
7611 let changeset = crate::sync::apply::ValidatedChangeset::new(
7612 package,
7613 schema.clone(),
7614 )
7615 .map_err(|error| {
7616 DbError::Message(format!("invalid Serial resolution changeset: {error}"))
7617 })?;
7618 crate::sync::apply::apply_changeset_strict_on(tx, changeset).map_err(|error| {
7619 DbError::Message(format!(
7620 "apply Serial resolution commit {}: {error}",
7621 resolution.commit.seq()
7622 ))
7623 })?;
7624 let blob_decls = BlobDecls::from_tables(tx, synced_tables)
7625 .map_err(|error| DbError::Message(error.to_string()))?;
7626 for intent in resolution.cleanup {
7627 crate::blob::local_cleanup::record_if_unreferenced_on(
7628 tx,
7629 &blob_decls,
7630 &intent,
7631 )?;
7632 }
7633 }
7634 Self::record_activated_store_device_registrations_on(
7635 tx,
7636 &resolution.commit,
7637 &resolution.registrations,
7638 )?;
7639 Self::record_verified_circle_activations_on(
7640 tx,
7641 &resolution.commit,
7642 &resolution.commit_ref,
7643 &resolution.circle_activations,
7644 )?;
7645 Self::record_materialized_serial_commit_on(
7646 tx,
7647 &resolution.commit,
7648 &resolution.commit_ref,
7649 &resolution.authorization_after,
7650 )?;
7651 predecessor = Some(resolution.commit_ref);
7652 }
7653 let head_commit = match plan.head.state {
7654 StoreSerialHeadState::Commit { commit, .. } => Some(commit),
7655 StoreSerialHeadState::Genesis { .. } => None,
7656 };
7657 if predecessor != head_commit {
7658 return Err(DbError::Message(
7659 "Serial resolution commits do not reach the verified global head".to_string(),
7660 ));
7661 }
7662 Ok(branch.into_iter().map(|(write_id, _)| write_id).collect())
7663 }
7664
7665 fn resolve_unpublished_writes_on(
7666 tx: &rusqlite::Transaction<'_>,
7667 write_ids: &[WriteId],
7668 resolution: &WriteResolution,
7669 ) -> Result<(), DbError> {
7670 let status = WriteStatus::Resolved(resolution.clone());
7671 for write_id in write_ids {
7672 let raw_prepared: Option<String> = tx
7673 .query_row(
7674 "SELECT prepared FROM store_writes WHERE write_id = ?1",
7675 [write_id.as_str()],
7676 |row| row.get(0),
7677 )
7678 .map_err(DbError::from)?;
7679 let mut removable = Vec::new();
7680 let mut candidate = None;
7681 if let Some(raw_prepared) = raw_prepared.as_deref() {
7682 let prepared: PreparedStoreWriteState = serde_json::from_str(raw_prepared)
7683 .map_err(|error| {
7684 DbError::Message(format!("resolved prepared write: {error}"))
7685 })?;
7686 match &prepared {
7687 PreparedStoreWriteState::MergeConcurrent { .. }
7688 | PreparedStoreWriteState::MergeAbandonment { .. } => {
7689 let merge = parse_prepared_merge_candidate_on(tx, &prepared)?
7690 .expect("matched Merge preparation");
7691 removable.push(remote_object_id(&merge.reference.object));
7692 removable.push(remote_object_id(merge.head_prepared.reference()));
7693 candidate = Some(merge.reference);
7694 }
7695 PreparedStoreWriteState::Serial { .. } => {
7696 if let Some(serial) = parse_prepared_serial_candidate(raw_prepared)? {
7697 removable.push(remote_object_id(&serial.reference.object));
7698 candidate = Some(serial.reference);
7699 }
7700 }
7701 PreparedStoreWriteState::SerialPreparing => {}
7702 }
7703 }
7704 let mut statement = tx
7705 .prepare(
7706 "SELECT remote_object_id FROM store_write_packages WHERE write_id = ?1
7707 UNION
7708 SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1",
7709 )
7710 .map_err(DbError::from)?;
7711 let indexed = statement
7712 .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
7713 .map_err(DbError::from)?
7714 .collect::<Result<Vec<_>, _>>()
7715 .map_err(DbError::from)?;
7716 drop(statement);
7717 for encoded in indexed {
7718 removable.push(encoded.parse().map_err(|error| {
7719 DbError::Message(format!("resolved remote object id: {error}"))
7720 })?);
7721 }
7722 if let Some(candidate) = &candidate {
7723 for object_id in &removable {
7724 let remote = load_remote_object_on(tx, *object_id)?;
7725 if !remote
7726 .candidate_cleanup_complete(candidate)
7727 .map_err(|error| {
7728 DbError::Message(format!(
7729 "validate candidate cleanup for {object_id}: {error}"
7730 ))
7731 })?
7732 {
7733 return Err(DbError::Message(format!(
7734 "candidate cleanup for remote object {object_id} is incomplete"
7735 )));
7736 }
7737 }
7738 }
7739 tx.execute(
7740 "DELETE FROM store_write_blob_leases WHERE write_id = ?1",
7741 [write_id.as_str()],
7742 )
7743 .map_err(DbError::from)?;
7744 tx.execute(
7745 "DELETE FROM store_write_packages WHERE write_id = ?1",
7746 [write_id.as_str()],
7747 )
7748 .map_err(DbError::from)?;
7749 tx.execute(
7750 "DELETE FROM store_write_blobs WHERE write_id = ?1",
7751 [write_id.as_str()],
7752 )
7753 .map_err(DbError::from)?;
7754 for object_id in removable {
7755 let remote = load_remote_object_on(tx, object_id)?;
7756 let absent = matches!(
7757 remote,
7758 RemoteObjectRecord::CandidateCommit(
7759 crate::sync::remote_object::CandidateCommitRecord {
7760 state:
7761 crate::sync::remote_object::CandidateCommitState::AbsentVerified { .. },
7762 ..
7763 }
7764 ) | RemoteObjectRecord::CandidateExclusive(
7765 crate::sync::remote_object::CandidateObjectRecord {
7766 state:
7767 crate::sync::remote_object::CandidateObjectState::AbsentVerified { .. },
7768 ..
7769 }
7770 ) | RemoteObjectRecord::RetainedAuthority(
7771 crate::sync::remote_object::RetainedAuthorityRecord {
7772 state:
7773 crate::sync::remote_object::RetainedAuthorityObjectState::UncreatedVerified { .. },
7774 ..
7775 }
7776 )
7777 );
7778 if absent {
7779 tx.execute(
7780 "DELETE FROM remote_objects WHERE object_id = ?1",
7781 [object_id.to_string()],
7782 )
7783 .map_err(DbError::from)?;
7784 }
7785 }
7786 tx.execute(
7787 "UPDATE store_writes SET prepared = NULL WHERE write_id = ?1",
7788 [write_id.as_str()],
7789 )
7790 .map_err(DbError::from)?;
7791 Self::set_write_status_on(tx, write_id, &status)?;
7792 }
7793 Ok(())
7794 }
7795
7796 #[doc(hidden)]
7797 pub async fn discard_pending_serial_branch(
7798 &self,
7799 branch_id: PendingBranchId,
7800 plan: crate::sync::store_pull::SerialResolutionPlan,
7801 ) -> Result<(), DbError> {
7802 let synced_tables = self.synced_tables().to_vec();
7803 let statuses = self.state.write_statuses.clone();
7804 self.call(move |conn| {
7805 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
7806 let write_ids =
7807 Self::apply_serial_resolution_on(&tx, &synced_tables, &branch_id, plan)?;
7808 let resolution = WriteResolution::Discarded;
7809 Self::resolve_unpublished_writes_on(&tx, &write_ids, &resolution)?;
7810 tx.commit().map_err(DbError::from)?;
7811 let status = WriteStatus::Resolved(resolution);
7812 for write_id in write_ids {
7813 Self::notify_write_status_in(&statuses, &write_id, status.clone());
7814 }
7815 Ok(())
7816 })
7817 .await
7818 }
7819
7820 #[doc(hidden)]
7821 pub async fn replace_pending_serial_branch<R, E, F>(
7822 &self,
7823 branch_id: PendingBranchId,
7824 plan: crate::sync::store_pull::SerialResolutionPlan,
7825 replacement_write_id: WriteId,
7826 f: F,
7827 ) -> Result<WriteReceipt<R>, E>
7828 where
7829 R: Send + 'static,
7830 E: From<DbError> + Send + 'static,
7831 F: FnOnce(&rusqlite::Transaction<'_>) -> Result<R, E> + Send + 'static,
7832 {
7833 let synced_tables = self.synced_tables().to_vec();
7834 let gates = self.gates();
7835 let blob_decls = self.blob_decls();
7836 let statuses = self.state.write_statuses.clone();
7837 let outcome = self
7838 .call(move |conn| {
7839 Ok((|| {
7840 let tx = conn
7841 .unchecked_transaction()
7842 .map_err(DbError::from)
7843 .map_err(E::from)?;
7844 let old_write_ids =
7845 Self::apply_serial_resolution_on(&tx, &synced_tables, &branch_id, plan)
7846 .map_err(E::from)?;
7847 let changes_before = tx.total_changes();
7848 let (value, captured) =
7849 Self::capture_host_changes_on(&tx, &synced_tables, || f(&tx))?;
7850 let partitions = Self::partition_captured_write_on(
7851 &tx,
7852 &captured,
7853 &gates,
7854 WritePolicy::Serial,
7855 StoreWriteRouting::SerialScoped,
7856 )
7857 .map_err(E::from)?;
7858 let blob_facts =
7859 Self::capture_partition_blob_facts_on(&tx, &partitions, &blob_decls)
7860 .map_err(E::from)?;
7861 let rows_changed = tx.total_changes().saturating_sub(changes_before);
7862 let inverse_changeset = Self::invert_changeset(&captured).map_err(E::from)?;
7863 let base = StoreWriteBase::Serial {
7864 branch_id: PendingBranchId::from_first_write(replacement_write_id.clone()),
7865 base: Self::latest_position_for_device_on(&tx, SERIAL_STREAM_ID)
7866 .map_err(E::from)?,
7867 };
7868 let status = Self::insert_store_write_on(
7869 &tx,
7870 &replacement_write_id,
7871 &partitions,
7872 &inverse_changeset,
7873 &base,
7874 &blob_facts,
7875 rows_changed,
7876 )
7877 .map_err(E::from)?;
7878 let pending_branch_id = (!matches!(&status, WriteStatus::LocalOnly))
7879 .then(|| PendingBranchId::from_first_write(replacement_write_id.clone()));
7880 let resolution = WriteResolution::Replaced {
7881 replacement: replacement_write_id.clone(),
7882 };
7883 Self::resolve_unpublished_writes_on(&tx, &old_write_ids, &resolution)
7884 .map_err(E::from)?;
7885 tx.commit().map_err(DbError::from).map_err(E::from)?;
7886 let old_status = WriteStatus::Resolved(resolution);
7887 for write_id in old_write_ids {
7888 Self::notify_write_status_in(&statuses, &write_id, old_status.clone());
7889 }
7890 Ok(WriteReceipt {
7891 value,
7892 write_id: replacement_write_id,
7893 status,
7894 pending_branch_id,
7895 })
7896 })())
7897 })
7898 .await
7899 .map_err(E::from)?;
7900 outcome
7901 }
7902
7903 pub(crate) async fn latest_local_store_ack(
7904 &self,
7905 ) -> Result<Option<PublishedStoreAck>, DbError> {
7906 self.call(load_published_store_ack_on).await
7907 }
7908
7909 pub(crate) async fn stage_store_ack(
7910 &self,
7911 ack: StoreAck,
7912 prepared: PreparedExactObject,
7913 ) -> Result<StoreAckRef, DbError> {
7914 self.call(move |conn| {
7915 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
7916 let (root, registration_ref, registration) = local_store_authority_on(&tx)?;
7917 if ack.author_registration != registration_ref {
7918 return Err(DbError::Message(
7919 "staged Store acknowledgement author differs from local activation".to_string(),
7920 ));
7921 }
7922 let reference = StoreAckRef {
7923 revision: ack.revision,
7924 ack_hash: ack.ack_hash(),
7925 object: prepared.reference().clone(),
7926 };
7927 let verified = StoreAck::parse_at(
7928 &ack.to_bytes(),
7929 root.store_root_hash,
7930 &reference,
7931 ®istration,
7932 )
7933 .map_err(|error| {
7934 DbError::Message(format!("verify staged Store acknowledgement: {error}"))
7935 })?;
7936 if verified != ack {
7937 return Err(DbError::Message(
7938 "staged Store acknowledgement changed during exact verification".to_string(),
7939 ));
7940 }
7941 let previous = load_published_store_ack_on(&tx)?;
7942 let (expected_revision, expected_predecessor, expected_slot) = match &previous {
7943 Some(previous) => (
7944 previous.reference.revision.checked_add(1).ok_or_else(|| {
7945 DbError::Message("Store acknowledgement revision overflow".to_string())
7946 })?,
7947 Some(previous.reference.clone()),
7948 previous.successor_slot.clone(),
7949 ),
7950 None => (1, None, store_ack_first_slot(®istration)?.clone()),
7951 };
7952 if ack.revision != expected_revision
7953 || ack.predecessor != expected_predecessor
7954 || prepared.reference().slot() != &expected_slot
7955 {
7956 return Err(DbError::Message(
7957 "Store acknowledgement does not extend the exact local stream".to_string(),
7958 ));
7959 }
7960 if ack.successor.predecessor
7961 != previous
7962 .as_ref()
7963 .map(|value| value.reference.object.clone())
7964 {
7965 return Err(DbError::Message(
7966 "Store acknowledgement successor names a different predecessor".to_string(),
7967 ));
7968 }
7969 let next_revision = ack.revision.checked_add(1).ok_or_else(|| {
7970 DbError::Message("Store acknowledgement revision overflow".to_string())
7971 })?;
7972 if ack.successor.activation
7973 != StreamActivationId::store_acknowledgements(&root, ®istration_ref)
7974 || ack.successor.next_slot.logical_key()
7975 != format!(
7976 "{}.json",
7977 ack_slot_prefix(®istration.device_id.to_string(), next_revision)
7978 )
7979 {
7980 return Err(DbError::Message(
7981 "Store acknowledgement successor is outside its activated exact stream"
7982 .to_string(),
7983 ));
7984 }
7985 let ack_ref = serde_json::to_string(&reference).map_err(|error| {
7986 DbError::Message(format!(
7987 "serialize exact Store acknowledgement ref: {error}"
7988 ))
7989 })?;
7990 let prepared = serde_json::to_string(&prepared).map_err(|error| {
7991 DbError::Message(format!("serialize prepared Store acknowledgement: {error}"))
7992 })?;
7993 tx.execute(
7994 "INSERT INTO outbound_store_acks \
7995 (singleton, ack_ref, ack_bytes, prepared_object) \
7996 VALUES (1, ?1, ?2, ?3)",
7997 rusqlite::params![ack_ref, ack.to_bytes(), prepared],
7998 )
7999 .map_err(DbError::from)?;
8000 tx.commit().map_err(DbError::from)?;
8001 Ok(reference)
8002 })
8003 .await
8004 }
8005
8006 pub(crate) async fn oldest_outbound_store_ack(
8007 &self,
8008 ) -> Result<Option<OutboundStoreAck>, DbError> {
8009 self.call(load_outbound_store_ack_on).await
8010 }
8011
8012 pub(crate) async fn complete_outbound_store_ack(
8013 &self,
8014 accepted: StoreAckRef,
8015 ) -> Result<(), DbError> {
8016 self.call(move |conn| {
8017 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8018 let outbound = load_outbound_store_ack_on(&tx)?.ok_or_else(|| {
8019 DbError::Message("outbound Store acknowledgement is absent".to_string())
8020 })?;
8021 if outbound.reference != accepted {
8022 return Err(DbError::Message(
8023 "accepted Store acknowledgement differs from the prepared exact object"
8024 .to_string(),
8025 ));
8026 }
8027 let deleted = tx
8028 .execute(
8029 "DELETE FROM outbound_store_acks WHERE singleton = 1 AND ack_ref = ?1",
8030 [serde_json::to_string(&accepted).map_err(|error| {
8031 DbError::Message(format!(
8032 "serialize accepted Store acknowledgement ref: {error}"
8033 ))
8034 })?],
8035 )
8036 .map_err(DbError::from)?;
8037 if deleted != 1 {
8038 return Err(DbError::Message(
8039 "outbound Store acknowledgement disappeared".to_string(),
8040 ));
8041 }
8042 let successor_slot = serde_json::to_string(&outbound.ack.value.successor.next_slot)
8043 .map_err(|error| {
8044 DbError::Message(format!(
8045 "serialize Store acknowledgement successor slot: {error}"
8046 ))
8047 })?;
8048 tx.execute(
8049 "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
8050 VALUES (1, ?1, ?2) \
8051 ON CONFLICT(singleton) DO UPDATE SET \
8052 ack_ref = excluded.ack_ref, successor_slot = excluded.successor_slot",
8053 (
8054 serde_json::to_string(&accepted).map_err(|error| {
8055 DbError::Message(format!(
8056 "serialize published Store acknowledgement ref: {error}"
8057 ))
8058 })?,
8059 successor_slot,
8060 ),
8061 )
8062 .map_err(DbError::from)?;
8063 tx.commit().map_err(DbError::from)
8064 })
8065 .await
8066 }
8067
8068 pub(crate) async fn outbound_membership_mutation(
8069 &self,
8070 ) -> Result<Option<DurableMembershipMutation>, DbError> {
8071 self.call(|conn| {
8072 conn.query_row(
8073 "SELECT intent_hash, plan_bytes, progress_bytes \
8074 FROM outbound_membership_mutation WHERE singleton = 1",
8075 [],
8076 |row| {
8077 Ok((
8078 row.get::<_, String>(0)?,
8079 row.get::<_, Vec<u8>>(1)?,
8080 row.get::<_, Vec<u8>>(2)?,
8081 ))
8082 },
8083 )
8084 .optional()
8085 .map_err(DbError::from)?
8086 .map(|(hash, plan_bytes, progress_bytes)| {
8087 let intent_hash: ObjectHash = hash.parse().map_err(|error| {
8088 DbError::Message(format!("membership intent hash: {error}"))
8089 })?;
8090 if ObjectHash::digest(&plan_bytes) != intent_hash {
8091 return Err(DbError::Message(
8092 "membership intent hash differs from its exact plan bytes".to_string(),
8093 ));
8094 }
8095 Ok(DurableMembershipMutation {
8096 intent_hash,
8097 plan_bytes,
8098 progress_bytes,
8099 })
8100 })
8101 .transpose()
8102 })
8103 .await
8104 }
8105
8106 pub(crate) async fn select_membership_author_stream(
8107 &self,
8108 author_pubkey: &str,
8109 author_owner_grant: &crate::sync::membership::MembershipGrantId,
8110 reusable: std::collections::BTreeSet<crate::sync::membership::AuthorStreamId>,
8111 ) -> Result<crate::sync::membership::AuthorStreamId, DbError> {
8112 self.select_causal_author_stream(
8113 format!("membership_author_stream/{author_pubkey}/{author_owner_grant}"),
8114 reusable,
8115 )
8116 .await
8117 }
8118
8119 pub(crate) async fn select_causal_author_stream(
8120 &self,
8121 key: String,
8122 reusable: std::collections::BTreeSet<crate::sync::membership::AuthorStreamId>,
8123 ) -> Result<crate::sync::membership::AuthorStreamId, DbError> {
8124 let candidate = crate::sync::membership::AuthorStreamId::generate(self.id_provider());
8125 self.call(move |conn| {
8126 let existing = conn
8127 .query_row(
8128 "SELECT value FROM protocol_state WHERE key = ?1",
8129 [&key],
8130 |row| row.get::<_, String>(0),
8131 )
8132 .optional()
8133 .map_err(DbError::from)?
8134 .map(|value| {
8135 value.parse().map_err(|error| {
8136 DbError::Message(format!(
8137 "membership author stream state is malformed: {error}"
8138 ))
8139 })
8140 })
8141 .transpose()?;
8142 if let Some(existing) = existing {
8143 if reusable.contains(&existing) {
8144 return Ok(existing);
8145 }
8146 }
8147 let selected = reusable.iter().next_back().copied().unwrap_or(candidate);
8148 conn.execute(
8149 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
8150 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
8151 rusqlite::params![key, selected.to_string()],
8152 )
8153 .map_err(DbError::from)?;
8154 Ok(selected)
8155 })
8156 .await
8157 }
8158
8159 pub(crate) async fn stage_membership_mutation(
8160 &self,
8161 plan_bytes: Vec<u8>,
8162 progress_bytes: Vec<u8>,
8163 ) -> Result<ObjectHash, DbError> {
8164 self.call(move |conn| {
8165 let intent_hash = ObjectHash::digest(&plan_bytes);
8166 let existing = conn
8167 .query_row(
8168 "SELECT intent_hash, plan_bytes FROM outbound_membership_mutation \
8169 WHERE singleton = 1",
8170 [],
8171 |row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)),
8172 )
8173 .optional()
8174 .map_err(DbError::from)?;
8175 if let Some((existing_hash, existing_plan)) = existing {
8176 if existing_hash == intent_hash.to_string() && existing_plan == plan_bytes {
8177 return Ok(intent_hash);
8178 }
8179 return Err(DbError::Message(
8180 "a different membership mutation is already pending".to_string(),
8181 ));
8182 }
8183 conn.execute(
8184 "INSERT INTO outbound_membership_mutation \
8185 (singleton, intent_hash, plan_bytes, progress_bytes) \
8186 VALUES (1, ?1, ?2, ?3)",
8187 rusqlite::params![intent_hash.to_string(), plan_bytes, progress_bytes],
8188 )
8189 .map_err(DbError::from)?;
8190 Ok(intent_hash)
8191 })
8192 .await
8193 }
8194
8195 pub(crate) async fn update_membership_mutation_progress(
8196 &self,
8197 intent_hash: ObjectHash,
8198 progress_bytes: Vec<u8>,
8199 ) -> Result<(), DbError> {
8200 self.call(move |conn| {
8201 let updated = conn
8202 .execute(
8203 "UPDATE outbound_membership_mutation SET progress_bytes = ?1 \
8204 WHERE singleton = 1 AND intent_hash = ?2",
8205 rusqlite::params![progress_bytes, intent_hash.to_string()],
8206 )
8207 .map_err(DbError::from)?;
8208 if updated != 1 {
8209 return Err(DbError::Message(
8210 "membership mutation ownership row is absent or changed".to_string(),
8211 ));
8212 }
8213 Ok(())
8214 })
8215 .await
8216 }
8217
8218 pub(crate) async fn complete_membership_mutation(
8219 &self,
8220 intent_hash: ObjectHash,
8221 ) -> Result<(), DbError> {
8222 self.call(move |conn| {
8223 let deleted = conn
8224 .execute(
8225 "DELETE FROM outbound_membership_mutation \
8226 WHERE singleton = 1 AND intent_hash = ?1",
8227 [intent_hash.to_string()],
8228 )
8229 .map_err(DbError::from)?;
8230 if deleted != 1 {
8231 return Err(DbError::Message(
8232 "membership mutation ownership row is absent or changed".to_string(),
8233 ));
8234 }
8235 Ok(())
8236 })
8237 .await
8238 }
8239
8240 pub(crate) async fn outbound_snapshot_publication(
8241 &self,
8242 ) -> Result<Option<DurableSnapshotPublication>, DbError> {
8243 let pending = self.call(load_outbound_store_snapshot_on).await?;
8244 if let Some(pending) = &pending {
8245 for blob in &pending.blobs {
8246 if let Some(spool_path) = &blob.spool_path {
8247 crate::local_blob::verify_exact_file(blob.remote.object(), spool_path)
8248 .await
8249 .map_err(|error| {
8250 DbError::Message(format!("prepared snapshot blob spool: {error}"))
8251 })?;
8252 }
8253 }
8254 }
8255 Ok(pending)
8256 }
8257
8258 pub(crate) async fn stage_snapshot_publication(
8259 &self,
8260 meta: SnapshotMeta,
8261 meta_prepared: PreparedExactObject,
8262 image_bytes: Vec<u8>,
8263 image_prepared: PreparedExactObject,
8264 blobs: Vec<PreparedSnapshotBlob>,
8265 ) -> Result<StoreSnapshotRef, DbError> {
8266 let synced_tables = self.synced_tables().to_vec();
8267 let gates = self.gates();
8268 self.call(move |conn| {
8269 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8270 let (root, registration_ref, registration) = local_store_authority_on(&tx)?;
8271 if meta.author_registration != registration_ref {
8272 return Err(DbError::Message(
8273 "staged Store snapshot author differs from local activation".to_string(),
8274 ));
8275 }
8276 if meta.image.object != *image_prepared.reference()
8277 || ObjectHash::digest(&image_bytes) != meta.image.image_hash
8278 || meta.image.object.slot().logical_key()
8279 != format!(
8280 "{}.db",
8281 snapshot_image_semantic_prefix(
8282 ®istration.device_id.to_string(),
8283 meta.image.image_hash,
8284 )
8285 )
8286 {
8287 return Err(DbError::Message(
8288 "staged Store snapshot image differs from its exact reference".to_string(),
8289 ));
8290 }
8291 let reference = StoreSnapshotRef {
8292 sequence: meta.sequence,
8293 snapshot_hash: meta.snapshot_hash(),
8294 object: meta_prepared.reference().clone(),
8295 };
8296 let verified = SnapshotMeta::parse_at(
8297 &meta.to_bytes(),
8298 root.store_root_hash,
8299 &reference,
8300 ®istration,
8301 )
8302 .map_err(|error| {
8303 DbError::Message(format!("verify staged Store snapshot metadata: {error}"))
8304 })?;
8305 if verified != meta {
8306 return Err(DbError::Message(
8307 "staged Store snapshot changed during exact verification".to_string(),
8308 ));
8309 }
8310 let previous = load_published_store_snapshot_on(&tx)?;
8311 let (expected_sequence, expected_predecessor, expected_slot) = match &previous {
8312 Some(previous) => (
8313 previous.reference.sequence.checked_add(1).ok_or_else(|| {
8314 DbError::Message("Store snapshot sequence overflow".to_string())
8315 })?,
8316 Some(previous.reference.clone()),
8317 previous.successor_slot.clone(),
8318 ),
8319 None => (1, None, store_snapshot_first_slot(®istration)?.clone()),
8320 };
8321 if meta.sequence != expected_sequence
8322 || meta.predecessor != expected_predecessor
8323 || meta_prepared.reference().slot() != &expected_slot
8324 || meta.successor.predecessor
8325 != previous
8326 .as_ref()
8327 .map(|value| value.reference.object.clone())
8328 {
8329 return Err(DbError::Message(
8330 "Store snapshot does not extend the exact local stream".to_string(),
8331 ));
8332 }
8333 let next_sequence = meta
8334 .sequence
8335 .checked_add(1)
8336 .ok_or_else(|| DbError::Message("Store snapshot sequence overflow".to_string()))?;
8337 if meta.successor.activation
8338 != StreamActivationId::store_snapshots(&root, ®istration_ref)
8339 || meta.successor.next_slot.logical_key()
8340 != format!(
8341 "{}.json",
8342 snapshot_slot_prefix(®istration.device_id.to_string(), next_sequence)
8343 )
8344 {
8345 return Err(DbError::Message(
8346 "Store snapshot successor is outside its activated exact stream".to_string(),
8347 ));
8348 }
8349 for blob in &blobs {
8350 validate_snapshot_blob_plan_on(conn, &gates, &synced_tables, &meta, blob)?;
8351 }
8352 tx.execute(
8353 "INSERT INTO outbound_store_snapshot \
8354 (singleton, snapshot_ref, meta_prepared, image_ref, image_prepared, \
8355 image_bytes, meta_bytes, blobs) \
8356 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7)",
8357 rusqlite::params![
8358 serde_json::to_string(&reference).map_err(|error| DbError::Message(
8359 format!("serialize exact Store snapshot ref: {error}")
8360 ))?,
8361 serde_json::to_string(&meta_prepared).map_err(|error| DbError::Message(
8362 format!("serialize prepared Store snapshot metadata: {error}")
8363 ))?,
8364 serde_json::to_string(&meta.image).map_err(|error| DbError::Message(
8365 format!("serialize exact Store snapshot image ref: {error}")
8366 ))?,
8367 serde_json::to_string(&image_prepared).map_err(|error| DbError::Message(
8368 format!("serialize prepared Store snapshot image: {error}")
8369 ))?,
8370 image_bytes,
8371 meta.to_bytes(),
8372 serde_json::to_string(&blobs).map_err(|error| DbError::Message(format!(
8373 "serialize prepared Store snapshot blobs: {error}"
8374 )))?,
8375 ],
8376 )
8377 .map_err(DbError::from)?;
8378 tx.commit().map_err(DbError::from)?;
8379 Ok(reference)
8380 })
8381 .await
8382 }
8383
8384 pub(crate) async fn latest_local_store_snapshot(
8385 &self,
8386 ) -> Result<Option<PublishedStoreSnapshot>, DbError> {
8387 self.call(load_published_store_snapshot_on).await
8388 }
8389
8390 pub(crate) async fn complete_snapshot_publication(
8391 &self,
8392 accepted: StoreSnapshotRef,
8393 ) -> Result<(), DbError> {
8394 self.call(move |conn| {
8395 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8396 let outbound = load_outbound_store_snapshot_on(&tx)?
8397 .ok_or_else(|| DbError::Message("outbound Store snapshot is absent".to_string()))?;
8398 if outbound.reference != accepted {
8399 return Err(DbError::Message(
8400 "accepted Store snapshot differs from the prepared exact object".to_string(),
8401 ));
8402 }
8403 for blob in &outbound.blobs {
8404 install_snapshot_blob_plan_on(&tx, blob)?;
8405 if let Some(path) = &blob.spool_path {
8406 tx.execute(
8407 "INSERT INTO snapshot_blob_spool_cleanup (path) VALUES (?1)
8408 ON CONFLICT(path) DO NOTHING",
8409 [path.to_string_lossy().as_ref()],
8410 )
8411 .map_err(DbError::from)?;
8412 }
8413 }
8414 let deleted = tx
8415 .execute(
8416 "DELETE FROM outbound_store_snapshot \
8417 WHERE singleton = 1 AND snapshot_ref = ?1",
8418 [serde_json::to_string(&accepted).map_err(|error| {
8419 DbError::Message(format!("serialize accepted Store snapshot ref: {error}"))
8420 })?],
8421 )
8422 .map_err(DbError::from)?;
8423 if deleted != 1 {
8424 return Err(DbError::Message(
8425 "outbound snapshot ownership row is absent or changed".to_string(),
8426 ));
8427 }
8428 tx.execute(
8429 "INSERT INTO published_store_snapshot \
8430 (singleton, snapshot_ref, successor_slot, meta_bytes) VALUES (1, ?1, ?2, ?3) \
8431 ON CONFLICT(singleton) DO UPDATE SET \
8432 snapshot_ref = excluded.snapshot_ref, \
8433 successor_slot = excluded.successor_slot, \
8434 meta_bytes = excluded.meta_bytes",
8435 rusqlite::params![
8436 serde_json::to_string(&accepted).map_err(|error| DbError::Message(format!(
8437 "serialize published Store snapshot ref: {error}"
8438 )))?,
8439 serde_json::to_string(&outbound.meta.value.successor.next_slot).map_err(
8440 |error| DbError::Message(format!(
8441 "serialize Store snapshot successor slot: {error}"
8442 ))
8443 )?,
8444 outbound.meta.bytes,
8445 ],
8446 )
8447 .map_err(DbError::from)?;
8448 tx.commit().map_err(DbError::from)
8449 })
8450 .await
8451 }
8452
8453 pub(crate) async fn snapshot_blob_spool_cleanup_paths(&self) -> Result<Vec<PathBuf>, DbError> {
8454 self.call(|conn| {
8455 let mut statement = conn
8456 .prepare("SELECT path FROM snapshot_blob_spool_cleanup ORDER BY path")
8457 .map_err(DbError::from)?;
8458 let paths = statement
8459 .query_map([], |row| row.get::<_, String>(0))
8460 .map_err(DbError::from)?
8461 .map(|row| row.map(PathBuf::from).map_err(DbError::from))
8462 .collect();
8463 paths
8464 })
8465 .await
8466 }
8467
8468 pub(crate) async fn complete_snapshot_blob_spool_cleanup(
8469 &self,
8470 path: &Path,
8471 ) -> Result<(), DbError> {
8472 let path = path.to_string_lossy().into_owned();
8473 self.call(move |conn| {
8474 let deleted = conn
8475 .execute(
8476 "DELETE FROM snapshot_blob_spool_cleanup WHERE path = ?1",
8477 [&path],
8478 )
8479 .map_err(DbError::from)?;
8480 if deleted != 1 {
8481 return Err(DbError::Message(
8482 "snapshot blob spool cleanup ownership is absent".to_string(),
8483 ));
8484 }
8485 Ok(())
8486 })
8487 .await
8488 }
8489
8490 pub async fn local_store_root_ref(
8491 &self,
8492 ) -> Result<Option<crate::sync::store_commit::StoreRootRef>, DbError> {
8493 self.call(|conn| {
8494 load_store_root_authority_on(conn)
8495 .map(|authority| authority.map(|(reference, _)| reference))
8496 })
8497 .await
8498 }
8499
8500 pub(crate) async fn install_store_root_authority(
8501 &self,
8502 reference: crate::sync::store_commit::StoreRootRef,
8503 bytes: Vec<u8>,
8504 ) -> Result<(), DbError> {
8505 self.call(move |conn| {
8506 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8507 install_store_root_authority_on(&tx, &reference, &bytes)?;
8508 tx.commit().map_err(DbError::from)
8509 })
8510 .await
8511 }
8512
8513 pub(crate) async fn install_store_owner_anchor(
8514 &self,
8515 root: crate::sync::store_commit::StoreRootRef,
8516 founder_reference: StoreDeviceRegistrationRef,
8517 founder: StoreDeviceRegistration,
8518 founder_bytes: Vec<u8>,
8519 genesis: ResolvedStoreDeviceState,
8520 owner: String,
8521 head_refs: Vec<crate::sync::membership::MembershipHeadRef>,
8522 ) -> Result<(), DbError> {
8523 if founder.author_pubkey != owner {
8524 return Err(DbError::Message(
8525 "Store founder registration author differs from the owner anchor".to_string(),
8526 ));
8527 }
8528 self.call(move |conn| {
8529 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8530 install_store_founder_state_on(
8531 &tx,
8532 &root,
8533 &founder_reference,
8534 &founder,
8535 &founder_bytes,
8536 &genesis,
8537 )?;
8538 tx.execute(
8539 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
8540 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
8541 rusqlite::params![crate::sync::membership_ops::OWNER_PUBKEY_STATE_KEY, owner,],
8542 )
8543 .map_err(DbError::from)?;
8544 for reference in &head_refs {
8545 crate::sync::membership_ops::upsert_head_cursor_on(&tx, reference)?;
8546 }
8547 tx.commit().map_err(DbError::from)
8548 })
8549 .await
8550 }
8551
8552 pub(crate) async fn local_store_founder_graph(
8553 &self,
8554 ) -> Result<Option<DurableFounderGraph>, DbError> {
8555 self.call(load_local_store_founder_graph_on).await
8556 }
8557
8558 pub(crate) async fn stage_store_founder_graph(
8559 &self,
8560 graph: DurableFounderGraph,
8561 ) -> Result<(), DbError> {
8562 validate_founder_graph(&graph)?;
8563 self.call(move |conn| {
8564 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8565 if let Some(existing) = load_local_store_founder_graph_on(&tx)? {
8566 validate_founder_graph(&existing)?;
8567 if founder_graph_identity(&existing) == founder_graph_identity(&graph) {
8568 return Ok(());
8569 }
8570 return Err(DbError::Message(
8571 "local Store founder graph already owns different exact objects".to_string(),
8572 ));
8573 }
8574 consume_store_creation_probes_on(&tx, &graph)?;
8575 tx.execute(
8576 "INSERT INTO local_store_protocol_root \
8577 (singleton, store_root_hash, store_protocol_root_bytes, prepared_object) \
8578 VALUES (1, ?1, ?2, ?3)",
8579 rusqlite::params![
8580 graph.root.value.object_hash().to_string(),
8581 graph.root.bytes,
8582 serde_json::to_string(&graph.root.prepared).map_err(|error| {
8583 DbError::Message(format!("serialize prepared Store root: {error}"))
8584 })?,
8585 ],
8586 )
8587 .map_err(DbError::from)?;
8588 tx.execute(
8589 "INSERT INTO local_store_device_registration \
8590 (singleton, device_id, registration_hash, registration_bytes, prepared_object, \
8591 initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state) \
8592 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
8593 rusqlite::params![
8594 graph.registration.value.device_id.to_string(),
8595 graph.registration.value.registration_hash().to_string(),
8596 graph.registration.bytes,
8597 serde_json::to_string(&graph.registration.prepared).map_err(|error| {
8598 DbError::Message(format!(
8599 "serialize prepared founder registration: {error}"
8600 ))
8601 })?,
8602 serde_json::to_string(&graph.initial_ack_ref).map_err(|error| {
8603 DbError::Message(format!("serialize founder initial ack ref: {error}"))
8604 })?,
8605 graph.initial_ack.bytes,
8606 serde_json::to_string(&graph.initial_ack.prepared).map_err(|error| {
8607 DbError::Message(format!("serialize founder initial ack object: {error}"))
8608 })?,
8609 serde_json::to_string(&LocalDeviceRegistrationState::Prepared).map_err(
8610 |error| DbError::Message(format!(
8611 "serialize registration journal state: {error}"
8612 ))
8613 )?,
8614 ],
8615 )
8616 .map_err(DbError::from)?;
8617 tx.execute(
8618 "INSERT INTO local_store_founder_graph \
8619 (singleton, membership_graph) VALUES (1, ?1)",
8620 rusqlite::params![serde_json::to_string(
8621 &DurableFounderMembershipJournal::from_graph(&graph.membership,)
8622 )
8623 .map_err(|error| DbError::Message(format!(
8624 "serialize founder membership graph: {error}"
8625 )))?,],
8626 )
8627 .map_err(DbError::from)?;
8628 tx.commit().map_err(DbError::from)
8629 })
8630 .await
8631 }
8632
8633 pub(crate) async fn complete_store_founder_graph(
8634 &self,
8635 expected_root: crate::sync::store_commit::StoreRootRef,
8636 expected_registration: StoreDeviceRegistrationRef,
8637 expected_initial_ack: StoreAckRef,
8638 expected_membership: FounderMembershipRefs,
8639 ) -> Result<(), DbError> {
8640 self.call(move |conn| {
8641 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
8642 let graph = load_local_store_founder_graph_on(&tx)?.ok_or_else(|| {
8643 DbError::Message("local Store founder graph is absent".to_string())
8644 })?;
8645 let root = crate::sync::store_commit::StoreRootRef {
8646 store_root_id: graph.root.value.descriptor.store_root_id(),
8647 store_root_hash: graph.root.value.object_hash(),
8648 object: graph.root.object.clone(),
8649 };
8650 let registration = StoreDeviceRegistrationRef::from_registration(
8651 &graph.registration.value,
8652 graph.registration.object.clone(),
8653 );
8654 if root != expected_root
8655 || registration != expected_registration
8656 || graph.initial_ack_ref != expected_initial_ack
8657 || match (&graph.membership, &expected_membership) {
8658 (
8659 DurableFounderMembership::MergeConcurrent {
8660 entry_ref,
8661 head_ref,
8662 ..
8663 },
8664 FounderMembershipRefs::MergeConcurrent { entry, head },
8665 ) => entry_ref != entry || head_ref != head,
8666 (DurableFounderMembership::Serial, FounderMembershipRefs::Serial) => false,
8667 _ => true,
8668 }
8669 {
8670 return Err(DbError::Message(
8671 "verified founder graph differs from its durable exact references".to_string(),
8672 ));
8673 }
8674 let founder_authority =
8675 crate::sync::store_commit::StoreDeviceRegistrationActivation::Founder {
8676 root: root.clone(),
8677 };
8678 let device_genesis = ResolvedStoreDeviceState::founder(
8679 &root,
8680 registration.clone(),
8681 &graph.root.value.descriptor.founder_pubkey,
8682 graph.root.value.descriptor.founder_grant.clone(),
8683 &graph.root.value.descriptor.founder_recovery,
8684 )
8685 .map_err(|error| DbError::Message(error.to_string()))?;
8686 let device_genesis_json = serde_json::to_string(&device_genesis).map_err(|error| {
8687 DbError::Message(format!("serialize Store device genesis state: {error}"))
8688 })?;
8689 let device_id = registration.device_id.to_string();
8690 let registration_hash = registration.registration_hash.to_string();
8691 match &graph.registration_state {
8692 LocalDeviceRegistrationState::Prepared => {
8693 return Err(DbError::Message(
8694 "founder registration and initial acknowledgement are not exact-created"
8695 .to_string(),
8696 ));
8697 }
8698 LocalDeviceRegistrationState::Created => {}
8699 LocalDeviceRegistrationState::Activated { authority } => {
8700 if authority != &founder_authority {
8701 return Err(DbError::Message(
8702 "founder registration journal carries another activation authority"
8703 .to_string(),
8704 ));
8705 }
8706 let installed = load_store_root_authority_on(&tx)?;
8707 let stored: Option<(String, String, String)> = tx
8708 .query_row(
8709 "SELECT registration_object, activation_authority, registration_hash \
8710 FROM store_device_registration_activations WHERE device_id = ?1",
8711 [&device_id],
8712 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
8713 )
8714 .optional()
8715 .map_err(DbError::from)?;
8716 let ack: Option<String> = tx
8717 .query_row(
8718 "SELECT ack_ref FROM published_store_acks WHERE singleton = 1",
8719 [],
8720 |row| row.get(0),
8721 )
8722 .optional()
8723 .map_err(DbError::from)?;
8724 let stored_device_genesis: Option<String> = tx
8725 .query_row(
8726 "SELECT value FROM protocol_state WHERE key = ?1",
8727 [STORE_DEVICE_GENESIS_STATE_KEY],
8728 |row| row.get(0),
8729 )
8730 .optional()
8731 .map_err(DbError::from)?;
8732 if installed
8733 .as_ref()
8734 .map(|(reference, value)| (reference, value))
8735 != Some((&root, &graph.root.value))
8736 || stored
8737 != Some((
8738 serde_json::to_string(®istration).map_err(|error| {
8739 DbError::Message(format!(
8740 "serialize founder registration ref: {error}"
8741 ))
8742 })?,
8743 serde_json::to_string(&founder_authority).map_err(|error| {
8744 DbError::Message(format!(
8745 "serialize founder authority: {error}"
8746 ))
8747 })?,
8748 registration.registration_hash.to_string(),
8749 ))
8750 || ack
8751 != Some(serde_json::to_string(&graph.initial_ack_ref).map_err(
8752 |error| {
8753 DbError::Message(format!("serialize founder ack ref: {error}"))
8754 },
8755 )?)
8756 || stored_device_genesis.as_deref() != Some(&device_genesis_json)
8757 {
8758 return Err(DbError::Message(
8759 "activated founder journal differs from installed exact authority"
8760 .to_string(),
8761 ));
8762 }
8763 return Ok(());
8764 }
8765 LocalDeviceRegistrationState::Retired { .. } => {
8766 return Err(DbError::Message(
8767 "founder graph cannot complete through a retired local registration".into(),
8768 ));
8769 }
8770 }
8771 install_store_root_authority_on(&tx, &root, &graph.root.bytes)?;
8772 let activation = serde_json::to_string(
8773 &crate::sync::store_commit::StoreDeviceRegistrationActivation::Founder {
8774 root: root.clone(),
8775 },
8776 )
8777 .map_err(|error| {
8778 DbError::Message(format!(
8779 "serialize founder registration activation: {error}"
8780 ))
8781 })?;
8782 let journal_state = serde_json::to_string(&LocalDeviceRegistrationState::Activated {
8783 authority: founder_authority,
8784 })
8785 .map_err(|error| {
8786 DbError::Message(format!("serialize founder registration journal: {error}"))
8787 })?;
8788 let updated = tx
8789 .execute(
8790 "UPDATE local_store_device_registration SET state = ?1 \
8791 WHERE singleton = 1 AND device_id = ?2 AND registration_hash = ?3 \
8792 AND initial_ack_ref = ?4 AND state = ?5",
8793 rusqlite::params![
8794 journal_state,
8795 &device_id,
8796 ®istration_hash,
8797 serde_json::to_string(&graph.initial_ack_ref).map_err(|error| {
8798 DbError::Message(format!("serialize founder initial ack ref: {error}"))
8799 })?,
8800 serde_json::to_string(&LocalDeviceRegistrationState::Created).map_err(
8801 |error| DbError::Message(format!(
8802 "serialize created journal state: {error}"
8803 ))
8804 )?,
8805 ],
8806 )
8807 .map_err(DbError::from)?;
8808 if updated != 1 {
8809 return Err(DbError::Message(
8810 "founder registration journal did not activate".to_string(),
8811 ));
8812 }
8813 tx.execute(
8814 "INSERT INTO store_device_registration_activations \
8815 (device_id, registration_hash, author_pubkey, device_signing_pubkey, \
8816 registration_bytes, registration_object, activation_authority) \
8817 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
8818 rusqlite::params![
8819 &device_id,
8820 ®istration_hash,
8821 graph.registration.value.author_pubkey,
8822 graph.registration.value.device_signing_pubkey,
8823 graph.registration.bytes,
8824 serde_json::to_string(®istration).map_err(|error| {
8825 DbError::Message(format!("serialize founder registration ref: {error}"))
8826 })?,
8827 activation,
8828 ],
8829 )
8830 .map_err(DbError::from)?;
8831 tx.execute(
8832 "INSERT INTO published_store_acks \
8833 (singleton, ack_ref, successor_slot) VALUES (1, ?1, ?2)",
8834 rusqlite::params![
8835 serde_json::to_string(&graph.initial_ack_ref).map_err(|error| {
8836 DbError::Message(format!("serialize founder initial ack ref: {error}"))
8837 })?,
8838 serde_json::to_string(&graph.initial_ack.value.successor.next_slot).map_err(
8839 |error| DbError::Message(format!(
8840 "serialize founder ack successor: {error}"
8841 ))
8842 )?,
8843 ],
8844 )
8845 .map_err(DbError::from)?;
8846 for (key, value) in [
8847 (LOCAL_DEVICE_ID_STATE_KEY, device_id),
8848 (STORE_DEVICE_GENESIS_STATE_KEY, device_genesis_json),
8849 ] {
8850 tx.execute(
8851 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
8852 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
8853 (key, value),
8854 )
8855 .map_err(DbError::from)?;
8856 }
8857 match &graph.membership {
8858 DurableFounderMembership::MergeConcurrent { head_ref, .. } => {
8859 tx.execute(
8860 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
8861 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
8862 (
8863 crate::sync::membership_ops::OWNER_PUBKEY_STATE_KEY,
8864 &graph.root.value.descriptor.founder_pubkey,
8865 ),
8866 )
8867 .map_err(DbError::from)?;
8868 crate::sync::membership_ops::upsert_head_cursor_on(&tx, head_ref)?;
8869 }
8870 DurableFounderMembership::Serial => {
8871 let authorization = SerialAuthorizationState::from_founder(
8872 &root,
8873 &graph.root.value,
8874 ®istration,
8875 &graph.registration.value,
8876 )
8877 .map_err(|error| DbError::Message(error.to_string()))?;
8878 Self::install_serial_root_authorization_on(
8879 &tx,
8880 &graph.root.value.descriptor.founder_pubkey,
8881 &authorization,
8882 )?;
8883 }
8884 }
8885 tx.commit().map_err(DbError::from)
8886 })
8887 .await
8888 }
8889
8890 pub(crate) async fn latest_local_store_device_registration(
8891 &self,
8892 ) -> Result<Option<DurableDeviceRegistration>, DbError> {
8893 self.read_local_store_device_registration(
8894 "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
8895 initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
8896 FROM local_store_device_registration WHERE singleton = 1",
8897 )
8898 .await
8899 }
8900
8901 pub async fn export_activated_device_continuation(
8902 &self,
8903 identity_signer: &crate::keys::UserKeypair,
8904 ) -> Result<crate::sync::restore_code::ActivatedContinuation, DbError> {
8905 let durable = self
8906 .latest_local_store_device_registration()
8907 .await?
8908 .ok_or_else(|| DbError::Message("local Store device registration is absent".into()))?;
8909 let LocalDeviceRegistrationState::Activated { authority } = durable.state else {
8910 return Err(DbError::Message(
8911 "local Store device registration is not activated".into(),
8912 ));
8913 };
8914 let root = self
8915 .local_store_root_ref()
8916 .await?
8917 .ok_or_else(DbError::missing_store_root_hash)?;
8918 let registration = StoreDeviceRegistration::parse_at(
8919 &durable.registration_bytes,
8920 &root,
8921 durable.device_id,
8922 )
8923 .map_err(|error| DbError::Message(format!("local Store registration: {error}")))?;
8924 let registration_ref = StoreDeviceRegistrationRef::from_registration(
8925 ®istration,
8926 durable.prepared.reference().clone(),
8927 );
8928 if registration_ref.registration_hash != durable.registration_hash {
8929 return Err(DbError::Message(
8930 "local Store registration hash differs from its exact object".into(),
8931 ));
8932 }
8933 let device_signer = registration
8934 .device_signer(identity_signer)
8935 .map_err(|error| DbError::Message(format!("local device signer: {error}")))?;
8936 let latest_ack = self
8937 .latest_local_store_ack()
8938 .await?
8939 .ok_or_else(|| DbError::Message("local Store acknowledgement is absent".into()))?;
8940 Ok(crate::sync::restore_code::ActivatedContinuation {
8941 identity_signing_secret: hex::encode(identity_signer.to_keypair_bytes()),
8942 device_signing_secret: hex::encode(device_signer.to_keypair_bytes()),
8943 registration: registration_ref,
8944 registration_bytes: durable.registration_bytes,
8945 registration_prepared: durable.prepared,
8946 initial_ack: durable.initial_ack_ref,
8947 initial_ack_bytes: durable.initial_ack.bytes,
8948 initial_ack_prepared: durable.initial_ack.prepared,
8949 activation: authority,
8950 latest_ack: latest_ack.reference,
8951 latest_snapshot: self
8952 .latest_local_store_snapshot()
8953 .await?
8954 .map(|snapshot| snapshot.reference),
8955 latest_position: self.latest_local_store_position().await?,
8956 })
8957 }
8958
8959 pub async fn install_activated_device_continuation(
8960 &self,
8961 continuation: crate::sync::restore_code::ActivatedContinuation,
8962 identity_signer: &crate::keys::UserKeypair,
8963 device_signer: &crate::keys::UserKeypair,
8964 ack_chain: Vec<(StoreAckRef, StoreAck)>,
8965 latest_snapshot: Option<(StoreSnapshotRef, SnapshotMeta)>,
8966 ) -> Result<(), DbError> {
8967 let root = self
8968 .local_store_root_ref()
8969 .await?
8970 .ok_or_else(DbError::missing_store_root_hash)?;
8971 let registration = StoreDeviceRegistration::parse_at(
8972 &continuation.registration_bytes,
8973 &root,
8974 continuation.registration.device_id,
8975 )
8976 .map_err(|error| DbError::Message(format!("continued Store registration: {error}")))?;
8977 continuation
8978 .registration
8979 .verify_registration(®istration)
8980 .map_err(|error| DbError::Message(error.to_string()))?;
8981 let derived_device = registration
8982 .device_signer(identity_signer)
8983 .map_err(|error| DbError::Message(format!("continued device signer: {error}")))?;
8984 if derived_device.to_keypair_bytes() != device_signer.to_keypair_bytes()
8985 || continuation.registration_prepared.reference() != &continuation.registration.object
8986 || continuation.initial_ack_prepared.reference() != &continuation.initial_ack.object
8987 {
8988 return Err(DbError::Message(
8989 "continued device keys or exact registration objects differ".into(),
8990 ));
8991 }
8992 let initial_ack = StoreAck::parse_at(
8993 &continuation.initial_ack_bytes,
8994 root.store_root_hash,
8995 &continuation.initial_ack,
8996 ®istration,
8997 )
8998 .map_err(|error| DbError::Message(format!("continued initial ack: {error}")))?;
8999 let Some((latest_ack_ref, latest_ack)) = ack_chain.first() else {
9000 return Err(DbError::Message(
9001 "continued acknowledgement chain is empty".into(),
9002 ));
9003 };
9004 if initial_ack.revision != 1
9005 || initial_ack.predecessor.is_some()
9006 || latest_ack.author_registration != continuation.registration
9007 || latest_ack_ref != &continuation.latest_ack
9008 || ack_chain.last().map(|(reference, _)| reference) != Some(&continuation.initial_ack)
9009 || ack_chain.windows(2).any(|pair| {
9010 pair[0].1.predecessor.as_ref() != Some(&pair[1].0)
9011 || pair[0].0.revision != pair[0].1.revision
9012 || pair[1].0.revision != pair[1].1.revision
9013 })
9014 {
9015 return Err(DbError::Message(
9016 "continued acknowledgement chain differs from its exact authority".into(),
9017 ));
9018 }
9019 let latest_successor_slot = latest_ack.successor.next_slot.clone();
9020 match (&continuation.latest_snapshot, &latest_snapshot) {
9021 (None, None) => {}
9022 (Some(expected), Some((reference, meta))) if expected == reference => {
9023 let verified = crate::sync::store_snapshot::verify_store_snapshot_bytes(
9024 &root,
9025 &continuation.registration,
9026 ®istration,
9027 reference,
9028 &meta.to_bytes(),
9029 )
9030 .map_err(|error| DbError::Message(error.to_string()))?;
9031 if verified != *meta {
9032 return Err(DbError::Message(
9033 "continued snapshot changed during exact verification".into(),
9034 ));
9035 }
9036 }
9037 _ => {
9038 return Err(DbError::Message(
9039 "continued snapshot stream differs from its exact authority".into(),
9040 ));
9041 }
9042 }
9043
9044 self.call(move |conn| {
9045 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9046 let activated = load_activated_registration_on(&tx, &root, &continuation.registration)?;
9047 if activated != registration {
9048 return Err(DbError::Message(
9049 "continued registration differs from activated Store state".into(),
9050 ));
9051 }
9052 let stored_authority: String = tx
9053 .query_row(
9054 "SELECT activation_authority FROM store_device_registration_activations \
9055 WHERE device_id = ?1 AND registration_hash = ?2",
9056 (
9057 continuation.registration.device_id.to_string(),
9058 continuation.registration.registration_hash.to_string(),
9059 ),
9060 |row| row.get(0),
9061 )
9062 .map_err(DbError::from)?;
9063 let stored_authority: crate::sync::store_commit::StoreDeviceRegistrationActivation =
9064 serde_json::from_str(&stored_authority).map_err(|error| {
9065 DbError::Message(format!("continued activation authority: {error}"))
9066 })?;
9067 if stored_authority != continuation.activation {
9068 return Err(DbError::Message(
9069 "continued registration has another activation authority".into(),
9070 ));
9071 }
9072 if let Some(position) = &continuation.latest_position {
9073 let stream_id = match &position.coord {
9074 StoreCommitCoord::MergeConcurrent { stream_id, .. } => stream_id.to_string(),
9075 StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
9076 };
9077 let restored_position = Self::latest_position_for_device_on(&tx, &stream_id)?;
9078 if restored_position.as_ref() != Some(position) {
9079 return Err(DbError::Message(
9080 "continued device position is absent from restored history".into(),
9081 ));
9082 }
9083 }
9084 let existing_local: i64 = tx
9085 .query_row(
9086 "SELECT COUNT(*) FROM local_store_device_registration",
9087 [],
9088 |row| row.get(0),
9089 )
9090 .map_err(DbError::from)?;
9091 let existing_ack: i64 = tx
9092 .query_row("SELECT COUNT(*) FROM published_store_acks", [], |row| {
9093 row.get(0)
9094 })
9095 .map_err(DbError::from)?;
9096 let existing_snapshot: i64 = tx
9097 .query_row("SELECT COUNT(*) FROM published_store_snapshot", [], |row| {
9098 row.get(0)
9099 })
9100 .map_err(DbError::from)?;
9101 let existing_device: Option<String> = tx
9102 .query_row(
9103 "SELECT value FROM protocol_state WHERE key = ?1",
9104 [LOCAL_DEVICE_ID_STATE_KEY],
9105 |row| row.get(0),
9106 )
9107 .optional()
9108 .map_err(DbError::from)?;
9109 let state = serde_json::to_string(&LocalDeviceRegistrationState::Activated {
9110 authority: continuation.activation.clone(),
9111 })
9112 .map_err(|error| DbError::Message(format!("continued activation: {error}")))?;
9113 let expected_local = (
9114 continuation.registration.device_id.to_string(),
9115 continuation.registration.registration_hash.to_string(),
9116 continuation.registration_bytes.clone(),
9117 serde_json::to_string(&continuation.registration_prepared).map_err(|error| {
9118 DbError::Message(format!("continued registration object: {error}"))
9119 })?,
9120 serde_json::to_string(&continuation.initial_ack).map_err(|error| {
9121 DbError::Message(format!("continued initial ack ref: {error}"))
9122 })?,
9123 continuation.initial_ack_bytes.clone(),
9124 serde_json::to_string(&continuation.initial_ack_prepared).map_err(|error| {
9125 DbError::Message(format!("continued initial ack object: {error}"))
9126 })?,
9127 state,
9128 );
9129 match existing_local {
9130 0 => {
9131 tx.execute(
9132 "INSERT INTO local_store_device_registration \
9133 (singleton, device_id, registration_hash, registration_bytes, \
9134 prepared_object, initial_ack_ref, initial_ack_bytes, \
9135 initial_ack_prepared, state) \
9136 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
9137 rusqlite::params![
9138 expected_local.0,
9139 expected_local.1,
9140 expected_local.2,
9141 expected_local.3,
9142 expected_local.4,
9143 expected_local.5,
9144 expected_local.6,
9145 expected_local.7,
9146 ],
9147 )
9148 .map_err(DbError::from)?;
9149 }
9150 1 => {
9151 let actual = tx
9152 .query_row(
9153 "SELECT device_id, registration_hash, registration_bytes, \
9154 prepared_object, initial_ack_ref, initial_ack_bytes, \
9155 initial_ack_prepared, state FROM local_store_device_registration \
9156 WHERE singleton = 1",
9157 [],
9158 |row| {
9159 Ok((
9160 row.get::<_, String>(0)?,
9161 row.get::<_, String>(1)?,
9162 row.get::<_, Vec<u8>>(2)?,
9163 row.get::<_, String>(3)?,
9164 row.get::<_, String>(4)?,
9165 row.get::<_, Vec<u8>>(5)?,
9166 row.get::<_, String>(6)?,
9167 row.get::<_, String>(7)?,
9168 ))
9169 },
9170 )
9171 .map_err(DbError::from)?;
9172 if actual != expected_local {
9173 return Err(DbError::Message(
9174 "restored local device state differs from continuation".into(),
9175 ));
9176 }
9177 }
9178 _ => {
9179 return Err(DbError::Message(
9180 "restored database carries multiple local device journals".into(),
9181 ));
9182 }
9183 }
9184 match existing_ack {
9185 0 => {}
9186 1 => {
9187 let (stored_ref, stored_successor): (String, String) = tx
9188 .query_row(
9189 "SELECT ack_ref, successor_slot FROM published_store_acks \
9190 WHERE singleton = 1",
9191 [],
9192 |row| Ok((row.get(0)?, row.get(1)?)),
9193 )
9194 .map_err(DbError::from)?;
9195 let stored_ref: StoreAckRef =
9196 serde_json::from_str(&stored_ref).map_err(|error| {
9197 DbError::Message(format!("restored acknowledgement: {error}"))
9198 })?;
9199 let Some((_, stored_ack)) = ack_chain
9200 .iter()
9201 .find(|(reference, _)| reference == &stored_ref)
9202 else {
9203 return Err(DbError::Message(
9204 "restored acknowledgement is outside the continuation chain".into(),
9205 ));
9206 };
9207 if stored_successor
9208 != serde_json::to_string(&stored_ack.successor.next_slot).map_err(
9209 |error| DbError::Message(format!("restored ack successor: {error}")),
9210 )?
9211 {
9212 return Err(DbError::Message(
9213 "restored acknowledgement successor differs from its signature".into(),
9214 ));
9215 }
9216 }
9217 _ => {
9218 return Err(DbError::Message(
9219 "restored database carries multiple local acknowledgements".into(),
9220 ));
9221 }
9222 }
9223 match (existing_snapshot, latest_snapshot.as_ref()) {
9224 (0, None) => {}
9225 (0, Some((reference, meta))) => {
9226 tx.execute(
9227 "INSERT INTO published_store_snapshot \
9228 (singleton, snapshot_ref, successor_slot, meta_bytes) \
9229 VALUES (1, ?1, ?2, ?3)",
9230 rusqlite::params![
9231 serde_json::to_string(reference).map_err(|error| {
9232 DbError::Message(format!(
9233 "serialize continued Store snapshot ref: {error}"
9234 ))
9235 })?,
9236 serde_json::to_string(&meta.successor.next_slot).map_err(|error| {
9237 DbError::Message(format!(
9238 "serialize continued Store snapshot successor: {error}"
9239 ))
9240 })?,
9241 meta.to_bytes(),
9242 ],
9243 )
9244 .map_err(DbError::from)?;
9245 }
9246 (1, Some((reference, meta))) => {
9247 let actual: (String, String, Vec<u8>) = tx
9248 .query_row(
9249 "SELECT snapshot_ref, successor_slot, meta_bytes \
9250 FROM published_store_snapshot WHERE singleton = 1",
9251 [],
9252 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
9253 )
9254 .map_err(DbError::from)?;
9255 let expected = (
9256 serde_json::to_string(reference).map_err(|error| {
9257 DbError::Message(format!(
9258 "serialize continued Store snapshot ref: {error}"
9259 ))
9260 })?,
9261 serde_json::to_string(&meta.successor.next_slot).map_err(|error| {
9262 DbError::Message(format!(
9263 "serialize continued Store snapshot successor: {error}"
9264 ))
9265 })?,
9266 meta.to_bytes(),
9267 );
9268 if actual != expected {
9269 return Err(DbError::Message(
9270 "restored local snapshot stream differs from continuation".into(),
9271 ));
9272 }
9273 }
9274 (1, None) => {
9275 return Err(DbError::Message(
9276 "restored database carries a snapshot outside the continuation".into(),
9277 ));
9278 }
9279 _ => {
9280 return Err(DbError::Message(
9281 "restored database carries multiple local snapshots".into(),
9282 ));
9283 }
9284 }
9285 let latest_ref = serde_json::to_string(&continuation.latest_ack)
9286 .map_err(|error| DbError::Message(format!("continued latest ack: {error}")))?;
9287 let latest_successor = serde_json::to_string(&latest_successor_slot)
9288 .map_err(|error| DbError::Message(format!("continued ack successor: {error}")))?;
9289 if existing_ack == 0 {
9290 tx.execute(
9291 "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
9292 VALUES (1, ?1, ?2)",
9293 (&latest_ref, &latest_successor),
9294 )
9295 .map_err(DbError::from)?;
9296 } else {
9297 tx.execute(
9298 "UPDATE published_store_acks SET ack_ref = ?1, successor_slot = ?2 \
9299 WHERE singleton = 1",
9300 (&latest_ref, &latest_successor),
9301 )
9302 .map_err(DbError::from)?;
9303 }
9304 match existing_device {
9305 Some(existing) if existing == continuation.registration.device_id.to_string() => {}
9306 Some(_) => {
9307 return Err(DbError::Message(
9308 "restored local device id differs from continuation".into(),
9309 ));
9310 }
9311 None => {
9312 tx.execute(
9313 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
9314 (
9315 LOCAL_DEVICE_ID_STATE_KEY,
9316 continuation.registration.device_id.to_string(),
9317 ),
9318 )
9319 .map_err(DbError::from)?;
9320 }
9321 }
9322 tx.commit().map_err(DbError::from)
9323 })
9324 .await
9325 }
9326
9327 pub(crate) async fn complete_merge_owner_recovery(
9328 &self,
9329 commit: StoreBatchCommit,
9330 commit_ref: StoreBatchCommitRef,
9331 registration: StoreDeviceRegistration,
9332 authority: crate::sync::store_commit::StoreDeviceRegistrationActivation,
9333 ) -> Result<(), DbError> {
9334 self.call(move |conn| {
9335 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9336 Self::record_activated_store_device_registrations_on(
9337 &tx,
9338 &commit,
9339 &[(registration, authority)],
9340 )?;
9341 Self::record_materialized_commit_on(&tx, &commit, &commit_ref)?;
9342 tx.commit().map_err(DbError::from)
9343 })
9344 .await
9345 }
9346
9347 pub(crate) async fn complete_serial_owner_recovery(
9348 &self,
9349 commit: StoreBatchCommit,
9350 commit_ref: StoreBatchCommitRef,
9351 registration: StoreDeviceRegistration,
9352 authority: crate::sync::store_commit::StoreDeviceRegistrationActivation,
9353 authorization: SerialAuthorizationState,
9354 ) -> Result<(), DbError> {
9355 self.call(move |conn| {
9356 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9357 Self::record_activated_store_device_registrations_on(
9358 &tx,
9359 &commit,
9360 &[(registration, authority)],
9361 )?;
9362 Self::record_materialized_serial_commit_on(&tx, &commit, &commit_ref, &authorization)?;
9363 tx.commit().map_err(DbError::from)
9364 })
9365 .await
9366 }
9367
9368 pub(crate) async fn stage_local_store_device_registration(
9369 &self,
9370 registration: ExactProtocolObject<StoreDeviceRegistration>,
9371 initial_ack_ref: StoreAckRef,
9372 initial_ack: ExactProtocolObject<StoreAck>,
9373 ) -> Result<(), DbError> {
9374 let registration_ref = StoreDeviceRegistrationRef::from_registration(
9375 ®istration.value,
9376 registration.object.clone(),
9377 );
9378 if registration.value.to_bytes() != registration.bytes
9379 || registration.object != *registration.prepared.reference()
9380 || initial_ack.value.to_bytes() != initial_ack.bytes
9381 || initial_ack.object != *initial_ack.prepared.reference()
9382 || initial_ack_ref.object != initial_ack.object
9383 || initial_ack_ref.ack_hash != initial_ack.value.ack_hash()
9384 || initial_ack_ref.revision != initial_ack.value.revision
9385 || initial_ack.value.author_registration != registration_ref
9386 {
9387 return Err(DbError::Message(
9388 "local registration staging graph contains mismatched exact objects".to_string(),
9389 ));
9390 }
9391 self.call(move |conn| {
9392 let root = required_store_root_authority_on(conn)?;
9393 if registration.value.store_root != root {
9394 return Err(DbError::Message(
9395 "local registration staging graph belongs to another Store root".to_string(),
9396 ));
9397 }
9398 let prepared = serde_json::to_string(®istration.prepared).map_err(|error| {
9399 DbError::Message(format!("serialize prepared local registration: {error}"))
9400 })?;
9401 let ack_ref = serde_json::to_string(&initial_ack_ref).map_err(|error| {
9402 DbError::Message(format!("serialize local initial ack ref: {error}"))
9403 })?;
9404 let ack_prepared = serde_json::to_string(&initial_ack.prepared).map_err(|error| {
9405 DbError::Message(format!("serialize prepared local initial ack: {error}"))
9406 })?;
9407 let existing: Option<PreparedLocalDeviceRegistrationRow> = conn
9408 .query_row(
9409 "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
9410 initial_ack_ref, initial_ack_bytes, initial_ack_prepared \
9411 FROM local_store_device_registration WHERE singleton = 1",
9412 [],
9413 |row| {
9414 Ok((
9415 row.get(0)?,
9416 row.get(1)?,
9417 row.get(2)?,
9418 row.get(3)?,
9419 row.get(4)?,
9420 row.get(5)?,
9421 row.get(6)?,
9422 ))
9423 },
9424 )
9425 .optional()
9426 .map_err(DbError::from)?;
9427 let expected = (
9428 registration_ref.device_id.to_string(),
9429 registration_ref.registration_hash.to_string(),
9430 registration.bytes.clone(),
9431 prepared.clone(),
9432 ack_ref.clone(),
9433 initial_ack.bytes.clone(),
9434 ack_prepared.clone(),
9435 );
9436 match existing {
9437 Some(existing) if existing == expected => Ok(()),
9438 Some(_) => Err(DbError::Message(
9439 "local registration journal already owns different exact objects".to_string(),
9440 )),
9441 None => conn
9442 .execute(
9443 "INSERT INTO local_store_device_registration \
9444 (singleton, device_id, registration_hash, registration_bytes, \
9445 prepared_object, initial_ack_ref, initial_ack_bytes, \
9446 initial_ack_prepared, state) \
9447 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
9448 rusqlite::params![
9449 expected.0,
9450 expected.1,
9451 expected.2,
9452 expected.3,
9453 expected.4,
9454 expected.5,
9455 expected.6,
9456 serde_json::to_string(&LocalDeviceRegistrationState::Prepared)
9457 .map_err(|error| DbError::Message(format!(
9458 "serialize local registration state: {error}"
9459 )))?,
9460 ],
9461 )
9462 .map(|_| ())
9463 .map_err(DbError::from),
9464 }
9465 })
9466 .await
9467 }
9468
9469 pub(crate) async fn install_existing_local_founder_device(
9470 &self,
9471 registration: ExactProtocolObject<StoreDeviceRegistration>,
9472 initial_ack_ref: StoreAckRef,
9473 initial_ack: ExactProtocolObject<StoreAck>,
9474 ) -> Result<(), DbError> {
9475 let registration_ref = StoreDeviceRegistrationRef::from_registration(
9476 ®istration.value,
9477 registration.object.clone(),
9478 );
9479 if registration.value.to_bytes() != registration.bytes
9480 || registration.object != *registration.prepared.reference()
9481 || initial_ack.value.to_bytes() != initial_ack.bytes
9482 || initial_ack.object != *initial_ack.prepared.reference()
9483 || initial_ack_ref.object != initial_ack.object
9484 || initial_ack_ref.ack_hash != initial_ack.value.ack_hash()
9485 || initial_ack_ref.revision != 1
9486 || initial_ack.value.revision != 1
9487 || initial_ack.value.predecessor.is_some()
9488 || initial_ack.value.author_registration != registration_ref
9489 {
9490 return Err(DbError::Message(
9491 "existing founder device graph contains mismatched exact objects".to_string(),
9492 ));
9493 }
9494 self.call(move |conn| {
9495 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9496 let root = required_store_root_authority_on(&tx)?;
9497 let crate::sync::store_commit::StoreDeviceRegistrationOrigin::Founder { .. } =
9498 ®istration.value.origin
9499 else {
9500 return Err(DbError::Message(
9501 "existing local founder device has a non-founder origin".to_string(),
9502 ));
9503 };
9504 if registration.value.store_root != root {
9505 return Err(DbError::Message(
9506 "existing local founder device belongs to another Store root".to_string(),
9507 ));
9508 }
9509 let activated = load_activated_registration_on(&tx, &root, ®istration_ref)?;
9510 if activated != registration.value {
9511 return Err(DbError::Message(
9512 "existing local founder device differs from its installed activation"
9513 .to_string(),
9514 ));
9515 }
9516 let authority = crate::sync::store_commit::StoreDeviceRegistrationActivation::Founder {
9517 root: root.clone(),
9518 };
9519 let expected = (
9520 registration_ref.device_id.to_string(),
9521 registration_ref.registration_hash.to_string(),
9522 registration.bytes.clone(),
9523 serde_json::to_string(®istration.prepared).map_err(|error| {
9524 DbError::Message(format!("serialize existing founder registration: {error}"))
9525 })?,
9526 serde_json::to_string(&initial_ack_ref).map_err(|error| {
9527 DbError::Message(format!("serialize existing founder ack ref: {error}"))
9528 })?,
9529 initial_ack.bytes.clone(),
9530 serde_json::to_string(&initial_ack.prepared).map_err(|error| {
9531 DbError::Message(format!("serialize existing founder ack object: {error}"))
9532 })?,
9533 serde_json::to_string(&LocalDeviceRegistrationState::Activated { authority })
9534 .map_err(|error| {
9535 DbError::Message(format!(
9536 "serialize existing founder registration state: {error}"
9537 ))
9538 })?,
9539 );
9540 tx.execute(
9541 "INSERT INTO local_store_device_registration
9542 (singleton, device_id, registration_hash, registration_bytes,
9543 prepared_object, initial_ack_ref, initial_ack_bytes,
9544 initial_ack_prepared, state)
9545 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
9546 ON CONFLICT(singleton) DO NOTHING",
9547 rusqlite::params![
9548 &expected.0,
9549 &expected.1,
9550 &expected.2,
9551 &expected.3,
9552 &expected.4,
9553 &expected.5,
9554 &expected.6,
9555 &expected.7,
9556 ],
9557 )
9558 .map_err(DbError::from)?;
9559 let stored: LocalDeviceRegistrationJournalRow = tx
9560 .query_row(
9561 "SELECT device_id, registration_hash, registration_bytes, prepared_object,
9562 initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state
9563 FROM local_store_device_registration WHERE singleton = 1",
9564 [],
9565 |row| {
9566 Ok((
9567 row.get(0)?,
9568 row.get(1)?,
9569 row.get(2)?,
9570 row.get(3)?,
9571 row.get(4)?,
9572 row.get(5)?,
9573 row.get(6)?,
9574 row.get(7)?,
9575 ))
9576 },
9577 )
9578 .map_err(DbError::from)?;
9579 if stored != expected {
9580 return Err(DbError::Message(
9581 "existing local founder journal owns different exact objects".to_string(),
9582 ));
9583 }
9584 let ack_ref = serde_json::to_string(&initial_ack_ref).map_err(|error| {
9585 DbError::Message(format!("serialize existing founder ack ref: {error}"))
9586 })?;
9587 let successor =
9588 serde_json::to_string(&initial_ack.value.successor.next_slot).map_err(|error| {
9589 DbError::Message(format!("serialize existing founder ack successor: {error}"))
9590 })?;
9591 tx.execute(
9592 "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot)
9593 VALUES (1, ?1, ?2) ON CONFLICT(singleton) DO NOTHING",
9594 (&ack_ref, &successor),
9595 )
9596 .map_err(DbError::from)?;
9597 let stored_ack: (String, String) = tx
9598 .query_row(
9599 "SELECT ack_ref, successor_slot FROM published_store_acks WHERE singleton = 1",
9600 [],
9601 |row| Ok((row.get(0)?, row.get(1)?)),
9602 )
9603 .map_err(DbError::from)?;
9604 if stored_ack != (ack_ref, successor) {
9605 return Err(DbError::Message(
9606 "existing local founder acknowledgement differs from exact cloud state"
9607 .to_string(),
9608 ));
9609 }
9610 tx.execute(
9611 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)
9612 ON CONFLICT(key) DO NOTHING",
9613 (LOCAL_DEVICE_ID_STATE_KEY, &expected.0),
9614 )
9615 .map_err(DbError::from)?;
9616 let stored_device_id: String = tx
9617 .query_row(
9618 "SELECT value FROM protocol_state WHERE key = ?1",
9619 [LOCAL_DEVICE_ID_STATE_KEY],
9620 |row| row.get(0),
9621 )
9622 .map_err(DbError::from)?;
9623 if stored_device_id != expected.0 {
9624 return Err(DbError::Message(
9625 "existing local founder device id conflicts with installed state".to_string(),
9626 ));
9627 }
9628 tx.commit().map_err(DbError::from)
9629 })
9630 .await
9631 }
9632
9633 pub(crate) async fn stage_owner_recovery_registration(
9634 &self,
9635 registration: ExactProtocolObject<StoreDeviceRegistration>,
9636 initial_ack_ref: StoreAckRef,
9637 initial_ack: ExactProtocolObject<StoreAck>,
9638 activation: crate::sync::store_commit::StoreDeviceRegistrationActivation,
9639 ) -> Result<bool, DbError> {
9640 let (
9641 crate::sync::store_commit::StoreDeviceRegistrationOrigin::Recovery {
9642 recovery_id: origin_recovery_id,
9643 recovery_slot,
9644 owner_grant,
9645 ..
9646 },
9647 crate::sync::store_commit::StoreDeviceRegistrationActivation::Recovery {
9648 recovery_id: activation_recovery_id,
9649 node,
9650 },
9651 ) = (®istration.value.origin, &activation)
9652 else {
9653 return Err(DbError::Message(
9654 "Owner recovery journal requires one Recovery registration authority".into(),
9655 ));
9656 };
9657 if origin_recovery_id != activation_recovery_id
9658 || node.object.slot() != recovery_slot
9659 || node.owner_grant != *owner_grant
9660 {
9661 return Err(DbError::Message(
9662 "Owner recovery registration differs from its activation authority".into(),
9663 ));
9664 }
9665 let registration_ref = StoreDeviceRegistrationRef::from_registration(
9666 ®istration.value,
9667 registration.object.clone(),
9668 );
9669 if registration.value.to_bytes() != registration.bytes
9670 || registration.object != *registration.prepared.reference()
9671 || initial_ack.value.to_bytes() != initial_ack.bytes
9672 || initial_ack.object != *initial_ack.prepared.reference()
9673 || initial_ack_ref.object != initial_ack.object
9674 || initial_ack_ref.ack_hash != initial_ack.value.ack_hash()
9675 || initial_ack_ref.revision != 1
9676 || initial_ack.value.predecessor.is_some()
9677 || initial_ack.value.author_registration != registration_ref
9678 {
9679 return Err(DbError::Message(
9680 "Owner recovery registration graph contains mismatched exact objects".into(),
9681 ));
9682 }
9683 self.call(move |conn| {
9684 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9685 let root = required_store_root_authority_on(&tx)?;
9686 if registration.value.store_root != root {
9687 return Err(DbError::Message(
9688 "Owner recovery registration belongs to another Store root".into(),
9689 ));
9690 }
9691 let exact_registration_ref =
9692 serde_json::to_string(®istration_ref).map_err(|error| {
9693 DbError::Message(format!("Owner recovery registration ref: {error}"))
9694 })?;
9695 let exact_activation = serde_json::to_string(&activation).map_err(|error| {
9696 DbError::Message(format!("Owner recovery activation authority: {error}"))
9697 })?;
9698 let activated = tx
9699 .query_row(
9700 "SELECT registration_hash, registration_bytes, registration_object, \
9701 activation_authority \
9702 FROM store_device_registration_activations WHERE device_id = ?1",
9703 [registration_ref.device_id.to_string()],
9704 |row| {
9705 Ok((
9706 row.get::<_, String>(0)?,
9707 row.get::<_, Vec<u8>>(1)?,
9708 row.get::<_, String>(2)?,
9709 row.get::<_, String>(3)?,
9710 ))
9711 },
9712 )
9713 .optional()
9714 .map_err(DbError::from)?;
9715 let activated = match activated {
9716 None => false,
9717 Some(existing)
9718 if existing
9719 == (
9720 registration_ref.registration_hash.to_string(),
9721 registration.bytes.clone(),
9722 exact_registration_ref,
9723 exact_activation,
9724 ) =>
9725 {
9726 true
9727 }
9728 Some(_) => {
9729 return Err(DbError::Message(
9730 "Owner recovery device already has different exact activation authority"
9731 .into(),
9732 ));
9733 }
9734 };
9735 tx.execute("DELETE FROM local_store_device_registration", [])
9736 .map_err(DbError::from)?;
9737 tx.execute("DELETE FROM published_store_acks", [])
9738 .map_err(DbError::from)?;
9739 tx.execute(
9740 "DELETE FROM protocol_state WHERE key = ?1",
9741 [LOCAL_DEVICE_ID_STATE_KEY],
9742 )
9743 .map_err(DbError::from)?;
9744 tx.execute(
9745 "INSERT INTO local_store_device_registration \
9746 (singleton, device_id, registration_hash, registration_bytes, \
9747 prepared_object, initial_ack_ref, initial_ack_bytes, \
9748 initial_ack_prepared, state) \
9749 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
9750 rusqlite::params![
9751 registration_ref.device_id.to_string(),
9752 registration_ref.registration_hash.to_string(),
9753 registration.bytes,
9754 serde_json::to_string(®istration.prepared).map_err(|error| {
9755 DbError::Message(format!("Owner recovery registration object: {error}"))
9756 })?,
9757 serde_json::to_string(&initial_ack_ref).map_err(|error| {
9758 DbError::Message(format!("Owner recovery initial ack ref: {error}"))
9759 })?,
9760 initial_ack.bytes,
9761 serde_json::to_string(&initial_ack.prepared).map_err(|error| {
9762 DbError::Message(format!("Owner recovery initial ack object: {error}"))
9763 })?,
9764 serde_json::to_string(&if activated {
9765 LocalDeviceRegistrationState::Activated {
9766 authority: activation,
9767 }
9768 } else {
9769 LocalDeviceRegistrationState::Prepared
9770 })
9771 .map_err(|error| DbError::Message(format!(
9772 "Owner recovery journal state: {error}"
9773 )))?,
9774 ],
9775 )
9776 .map_err(DbError::from)?;
9777 if activated {
9778 tx.execute(
9779 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
9780 (
9781 LOCAL_DEVICE_ID_STATE_KEY,
9782 registration_ref.device_id.to_string(),
9783 ),
9784 )
9785 .map_err(DbError::from)?;
9786 tx.execute(
9787 "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
9788 VALUES (1, ?1, ?2)",
9789 rusqlite::params![
9790 serde_json::to_string(&initial_ack_ref).map_err(|error| {
9791 DbError::Message(format!(
9792 "Owner recovery published initial ack ref: {error}"
9793 ))
9794 })?,
9795 serde_json::to_string(&initial_ack.value.successor.next_slot).map_err(
9796 |error| DbError::Message(format!(
9797 "Owner recovery initial ack successor: {error}"
9798 ))
9799 )?,
9800 ],
9801 )
9802 .map_err(DbError::from)?;
9803 }
9804 tx.commit().map_err(DbError::from)?;
9805 Ok(activated)
9806 })
9807 .await
9808 }
9809
9810 pub(crate) async fn stage_local_store_device_retirement(
9811 &self,
9812 payload: Vec<u8>,
9813 ) -> Result<(), DbError> {
9814 self.call(move |conn| {
9815 let existing: Option<(Vec<u8>, i64)> = conn
9816 .query_row(
9817 "SELECT payload, published FROM local_store_device_retirement WHERE singleton = 1",
9818 [],
9819 |row| Ok((row.get(0)?, row.get(1)?)),
9820 )
9821 .optional()
9822 .map_err(DbError::from)?;
9823 match existing {
9824 Some((existing, _)) if existing == payload => Ok(()),
9825 Some(_) => Err(DbError::Message(
9826 "local Store device retirement already owns different exact objects".into(),
9827 )),
9828 None => conn
9829 .execute(
9830 "INSERT INTO local_store_device_retirement (singleton, payload, published) VALUES (1, ?1, 0)",
9831 [payload],
9832 )
9833 .map(|_| ())
9834 .map_err(DbError::from),
9835 }
9836 })
9837 .await
9838 }
9839
9840 pub(crate) async fn local_store_device_retirement(
9841 &self,
9842 ) -> Result<Option<(Vec<u8>, bool)>, DbError> {
9843 self.call(|conn| {
9844 conn.query_row(
9845 "SELECT payload, published FROM local_store_device_retirement WHERE singleton = 1",
9846 [],
9847 |row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, i64>(1)? != 0)),
9848 )
9849 .optional()
9850 .map_err(DbError::from)
9851 })
9852 .await
9853 }
9854
9855 pub(crate) async fn complete_local_store_device_retirement(
9856 &self,
9857 expected_payload: Vec<u8>,
9858 retirement: crate::sync::store_commit::StoreDeviceSelfRetirementRef,
9859 commit: StoreBatchCommit,
9860 commit_ref: StoreBatchCommitRef,
9861 serial_authorization: Option<SerialAuthorizationState>,
9862 ) -> Result<(), DbError> {
9863 self.call(move |conn| {
9864 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
9865 let (payload, published): (Vec<u8>, i64) = tx
9866 .query_row(
9867 "SELECT payload, published FROM local_store_device_retirement WHERE singleton = 1",
9868 [],
9869 |row| Ok((row.get(0)?, row.get(1)?)),
9870 )
9871 .map_err(DbError::from)?;
9872 if payload != expected_payload {
9873 return Err(DbError::Message(
9874 "local Store device retirement changed exact ownership".into(),
9875 ));
9876 }
9877 if published == 1 {
9878 return Ok(());
9879 }
9880 match serial_authorization {
9881 Some(authorization) => Self::record_materialized_serial_commit_on(
9882 &tx,
9883 &commit,
9884 &commit_ref,
9885 &authorization,
9886 )?,
9887 None => Self::record_materialized_commit_on(&tx, &commit, &commit_ref)?,
9888 }
9889 let state: String = tx
9890 .query_row(
9891 "SELECT state FROM local_store_device_registration WHERE singleton = 1",
9892 [],
9893 |row| row.get(0),
9894 )
9895 .map_err(DbError::from)?;
9896 let LocalDeviceRegistrationState::Activated { authority } =
9897 serde_json::from_str(&state).map_err(|error| {
9898 DbError::Message(format!("parse active registration journal: {error}"))
9899 })?
9900 else {
9901 return Err(DbError::Message(
9902 "only an activated local registration can retire".into(),
9903 ));
9904 };
9905 tx.execute(
9906 "UPDATE local_store_device_registration SET state = ?1 WHERE singleton = 1",
9907 [serde_json::to_string(&LocalDeviceRegistrationState::Retired {
9908 authority,
9909 retirement,
9910 })
9911 .map_err(|error| DbError::Message(format!("serialize retired registration: {error}")))?],
9912 )
9913 .map_err(DbError::from)?;
9914 tx.execute(
9915 "UPDATE local_store_device_retirement SET published = 1 WHERE singleton = 1 AND published = 0",
9916 [],
9917 )
9918 .map_err(DbError::from)?;
9919 tx.commit().map_err(DbError::from)
9920 })
9921 .await
9922 }
9923
9924 pub(crate) async fn oldest_unpublished_store_device_registration(
9925 &self,
9926 ) -> Result<Option<DurableDeviceRegistration>, DbError> {
9927 self.read_local_store_device_registration(
9928 "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
9929 initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
9930 FROM local_store_device_registration WHERE singleton = 1 AND state = '\"prepared\"'",
9931 )
9932 .await
9933 }
9934
9935 async fn read_local_store_device_registration(
9936 &self,
9937 sql: &'static str,
9938 ) -> Result<Option<DurableDeviceRegistration>, DbError> {
9939 self.call(move |conn| {
9940 conn.query_row(sql, [], |row| {
9941 Ok((
9942 row.get::<_, String>(0)?,
9943 row.get::<_, String>(1)?,
9944 row.get::<_, Vec<u8>>(2)?,
9945 row.get::<_, String>(3)?,
9946 row.get::<_, String>(4)?,
9947 row.get::<_, Vec<u8>>(5)?,
9948 row.get::<_, String>(6)?,
9949 row.get::<_, String>(7)?,
9950 ))
9951 })
9952 .optional()
9953 .map_err(DbError::from)?
9954 .map(
9955 |(device_id, hash, bytes, prepared, ack_ref, ack_bytes, ack_prepared, state)| {
9956 let device_id = device_id.parse().map_err(|error| {
9957 DbError::Message(format!("local Store device id: {error}"))
9958 })?;
9959 let prepared: PreparedExactObject =
9960 serde_json::from_str(&prepared).map_err(|error| {
9961 DbError::Message(format!(
9962 "local Store device registration prepared object: {error}"
9963 ))
9964 })?;
9965 let registration = StoreDeviceRegistration::parse_at(
9966 &bytes,
9967 &required_store_root_authority_on(conn)?,
9968 device_id,
9969 )
9970 .map_err(|error| {
9971 DbError::Message(format!("local Store device registration: {error}"))
9972 })?;
9973 let initial_ack_ref: StoreAckRef =
9974 serde_json::from_str(&ack_ref).map_err(|error| {
9975 DbError::Message(format!(
9976 "local Store initial acknowledgement ref: {error}"
9977 ))
9978 })?;
9979 let initial_ack_value = StoreAck::parse_at(
9980 &ack_bytes,
9981 registration.store_root.store_root_hash,
9982 &initial_ack_ref,
9983 ®istration,
9984 )
9985 .map_err(|error| {
9986 DbError::Message(format!("local Store initial acknowledgement: {error}"))
9987 })?;
9988 let initial_ack_prepared: PreparedExactObject =
9989 serde_json::from_str(&ack_prepared).map_err(|error| {
9990 DbError::Message(format!("local Store initial ack object: {error}"))
9991 })?;
9992 Ok(DurableDeviceRegistration {
9993 device_id,
9994 registration_hash: hash.parse().map_err(|error| {
9995 DbError::Message(format!(
9996 "local Store device registration hash: {error}"
9997 ))
9998 })?,
9999 registration_bytes: bytes,
10000 prepared,
10001 initial_ack_ref,
10002 initial_ack: ExactProtocolObject {
10003 value: initial_ack_value,
10004 bytes: ack_bytes,
10005 object: initial_ack_prepared.reference().clone(),
10006 prepared: initial_ack_prepared,
10007 },
10008 state: serde_json::from_str(&state).map_err(|error| {
10009 DbError::Message(format!(
10010 "local Store registration journal state: {error}"
10011 ))
10012 })?,
10013 })
10014 },
10015 )
10016 .transpose()
10017 })
10018 .await
10019 }
10020
10021 pub(crate) async fn mark_local_store_device_registration_created(
10022 &self,
10023 registration: ExactProtocolObject<StoreDeviceRegistration>,
10024 initial_ack: StoreAckRef,
10025 initial_ack_object: ExactProtocolObject<StoreAck>,
10026 ) -> Result<(), DbError> {
10027 self.call(move |conn| {
10028 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10029 let registration_ref = StoreDeviceRegistrationRef::from_registration(
10030 ®istration.value,
10031 registration.object.clone(),
10032 );
10033 if registration_ref.object != *registration.prepared.reference()
10034 || initial_ack.object != *initial_ack_object.prepared.reference()
10035 {
10036 return Err(DbError::Message(
10037 "created registration refs differ from their prepared objects".to_string(),
10038 ));
10039 }
10040 let durable: (Vec<u8>, String, String, Vec<u8>, String, String) = tx
10041 .query_row(
10042 "SELECT registration_bytes, prepared_object, initial_ack_ref, \
10043 initial_ack_bytes, initial_ack_prepared, state \
10044 FROM local_store_device_registration \
10045 WHERE singleton = 1 AND device_id = ?1 AND registration_hash = ?2",
10046 (
10047 registration_ref.device_id.to_string(),
10048 registration_ref.registration_hash.to_string(),
10049 ),
10050 |row| {
10051 Ok((
10052 row.get(0)?,
10053 row.get(1)?,
10054 row.get(2)?,
10055 row.get(3)?,
10056 row.get(4)?,
10057 row.get(5)?,
10058 ))
10059 },
10060 )
10061 .map_err(DbError::from)?;
10062 let stored_registration_prepared: PreparedExactObject =
10063 serde_json::from_str(&durable.1).map_err(|error| {
10064 DbError::Message(format!("stored registration object: {error}"))
10065 })?;
10066 let stored_ack_ref: StoreAckRef = serde_json::from_str(&durable.2)
10067 .map_err(|error| DbError::Message(format!("stored initial ack ref: {error}")))?;
10068 let stored_ack_prepared: PreparedExactObject = serde_json::from_str(&durable.4)
10069 .map_err(|error| DbError::Message(format!("stored initial ack object: {error}")))?;
10070 if registration_ref.object != *stored_registration_prepared.reference()
10071 || registration.prepared.reference() != stored_registration_prepared.reference()
10072 || registration.prepared.stored_bytes()
10073 != stored_registration_prepared.stored_bytes()
10074 || registration.bytes != durable.0
10075 || initial_ack != stored_ack_ref
10076 || initial_ack_object.prepared.reference() != stored_ack_prepared.reference()
10077 || initial_ack_object.prepared.stored_bytes() != stored_ack_prepared.stored_bytes()
10078 || initial_ack_object.bytes != durable.3
10079 {
10080 return Err(DbError::Message(
10081 "created registration differs from its complete durable exact objects"
10082 .to_string(),
10083 ));
10084 }
10085 let prepared = serde_json::to_string(&LocalDeviceRegistrationState::Prepared).map_err(
10086 |error| DbError::Message(format!("serialize prepared journal: {error}")),
10087 )?;
10088 let created = serde_json::to_string(&LocalDeviceRegistrationState::Created)
10089 .map_err(|error| DbError::Message(format!("serialize created journal: {error}")))?;
10090 let updated = tx
10091 .execute(
10092 "UPDATE local_store_device_registration SET state = ?1 \
10093 WHERE singleton = 1 AND device_id = ?2 AND registration_hash = ?3 \
10094 AND initial_ack_ref = ?4 AND state = ?5",
10095 rusqlite::params![
10096 created,
10097 registration_ref.device_id.to_string(),
10098 registration_ref.registration_hash.to_string(),
10099 serde_json::to_string(&initial_ack).map_err(|error| {
10100 DbError::Message(format!("serialize initial ack ref: {error}"))
10101 })?,
10102 prepared,
10103 ],
10104 )
10105 .map_err(DbError::from)?;
10106 if updated != 1 {
10107 let already: Option<String> = tx
10108 .query_row(
10109 "SELECT state FROM local_store_device_registration \
10110 WHERE singleton = 1 AND device_id = ?1 AND registration_hash = ?2",
10111 (
10112 registration_ref.device_id.to_string(),
10113 registration_ref.registration_hash.to_string(),
10114 ),
10115 |row| row.get(0),
10116 )
10117 .optional()
10118 .map_err(DbError::from)?;
10119 if already.as_deref() != Some(created.as_str()) {
10120 return Err(DbError::Message(
10121 "local registration journal is absent or differs from the created object"
10122 .to_string(),
10123 ));
10124 }
10125 }
10126 tx.commit().map_err(DbError::from)
10127 })
10128 .await
10129 }
10130
10131 fn required_store_root_hash_on(conn: &Connection) -> Result<ObjectHash, DbError> {
10132 load_store_root_authority_on(conn)?
10133 .map(|(reference, _)| reference.store_root_hash)
10134 .ok_or_else(DbError::missing_store_root_hash)
10135 }
10136
10137 #[cfg(test)]
10138 pub(crate) async fn required_store_root_hash(&self) -> Result<ObjectHash, DbError> {
10139 self.call(Self::required_store_root_hash_on).await
10140 }
10141
10142 pub async fn get_protocol_state(&self, key: &str) -> Result<Option<String>, DbError> {
10143 let key = key.to_string();
10144 self.call(move |conn| {
10145 conn.query_row(
10146 "SELECT value FROM protocol_state WHERE key = ?1",
10147 [key],
10148 |r| r.get::<_, String>(0),
10149 )
10150 .optional()
10151 .map_err(DbError::from)
10152 })
10153 .await
10154 }
10155
10156 pub(crate) async fn begin_store_creation_attempt(
10157 &self,
10158 initialized: crate::sync::store_protocol_root::StoreCreationAttempt,
10159 ) -> Result<crate::sync::store_protocol_root::StoreCreationAttempt, DbError> {
10160 let value = serde_json::to_string(&initialized).map_err(|error| {
10161 DbError::Message(format!("serialize Store creation attempt: {error}"))
10162 })?;
10163 self.call(move |conn| {
10164 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10165 tx.execute(
10166 "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
10167 (
10168 crate::sync::store_protocol_root::STORE_CREATION_ATTEMPT_STATE_KEY,
10169 &value,
10170 ),
10171 )
10172 .map_err(DbError::from)?;
10173 let actual: String = tx
10174 .query_row(
10175 "SELECT value FROM protocol_state WHERE key = ?1",
10176 [crate::sync::store_protocol_root::STORE_CREATION_ATTEMPT_STATE_KEY],
10177 |row| row.get(0),
10178 )
10179 .map_err(DbError::from)?;
10180 tx.commit().map_err(DbError::from)?;
10181 serde_json::from_str(&actual)
10182 .map_err(|error| DbError::Message(format!("parse Store creation attempt: {error}")))
10183 })
10184 .await
10185 }
10186
10187 pub(crate) async fn load_store_creation_attempt(
10188 &self,
10189 ) -> Result<Option<crate::sync::store_protocol_root::StoreCreationAttempt>, DbError> {
10190 self.call(move |conn| {
10191 let value = conn
10192 .query_row(
10193 "SELECT value FROM protocol_state WHERE key = ?1",
10194 [crate::sync::store_protocol_root::STORE_CREATION_ATTEMPT_STATE_KEY],
10195 |row| row.get::<_, String>(0),
10196 )
10197 .optional()
10198 .map_err(DbError::from)?;
10199 value
10200 .map(|value| {
10201 serde_json::from_str(&value).map_err(|error| {
10202 DbError::Message(format!("parse Store creation attempt: {error}"))
10203 })
10204 })
10205 .transpose()
10206 })
10207 .await
10208 }
10209
10210 pub(crate) async fn advance_store_creation_attempt(
10211 &self,
10212 previous: crate::sync::store_protocol_root::StoreCreationAttempt,
10213 next: crate::sync::store_protocol_root::StoreCreationAttempt,
10214 ) -> Result<(), DbError> {
10215 let previous = serde_json::to_string(&previous).map_err(|error| {
10216 DbError::Message(format!("serialize Store creation predecessor: {error}"))
10217 })?;
10218 let next = serde_json::to_string(&next).map_err(|error| {
10219 DbError::Message(format!("serialize Store creation successor: {error}"))
10220 })?;
10221 self.call(move |conn| {
10222 let changed = conn
10223 .execute(
10224 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
10225 (
10226 &next,
10227 crate::sync::store_protocol_root::STORE_CREATION_ATTEMPT_STATE_KEY,
10228 &previous,
10229 ),
10230 )
10231 .map_err(DbError::from)?;
10232 if changed != 1 {
10233 return Err(DbError::Message(
10234 "Store creation attempt advance lost its exact predecessor".to_string(),
10235 ));
10236 }
10237 Ok(())
10238 })
10239 .await
10240 }
10241
10242 pub(crate) async fn load_provider_probe_journal(
10243 &self,
10244 probe_id: crate::sync::provider::ProviderProbeId,
10245 ) -> Result<Option<crate::sync::provider::ProviderProbeJournalRecord>, DbError> {
10246 let key = format!("provider_probe/{}", hex::encode(probe_id.as_bytes()));
10247 self.call(move |conn| {
10248 let value = conn
10249 .query_row(
10250 "SELECT value FROM protocol_state WHERE key = ?1",
10251 [key],
10252 |row| row.get::<_, String>(0),
10253 )
10254 .optional()
10255 .map_err(DbError::from)?;
10256 value
10257 .map(|value| {
10258 serde_json::from_str(&value).map_err(|error| {
10259 DbError::Message(format!("parse provider probe journal: {error}"))
10260 })
10261 })
10262 .transpose()
10263 })
10264 .await
10265 }
10266
10267 pub(crate) async fn begin_provider_probe_journal(
10268 &self,
10269 prepared: crate::sync::provider::ProviderProbeJournalRecord,
10270 ) -> Result<crate::sync::provider::ProviderProbeJournalRecord, DbError> {
10271 prepared.validate_begin().map_err(|error| {
10272 DbError::Message(format!("invalid provider probe journal beginning: {error}"))
10273 })?;
10274 let key = format!(
10275 "provider_probe/{}",
10276 hex::encode(prepared.probe_id().as_bytes())
10277 );
10278 let value = serde_json::to_string(&prepared).map_err(|error| {
10279 DbError::Message(format!("serialize provider probe journal: {error}"))
10280 })?;
10281 self.call(move |conn| {
10282 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10283 tx.execute(
10284 "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
10285 (&key, &value),
10286 )
10287 .map_err(DbError::from)?;
10288 let actual: String = tx
10289 .query_row(
10290 "SELECT value FROM protocol_state WHERE key = ?1",
10291 [&key],
10292 |row| row.get(0),
10293 )
10294 .map_err(DbError::from)?;
10295 tx.commit().map_err(DbError::from)?;
10296 serde_json::from_str(&actual)
10297 .map_err(|error| DbError::Message(format!("parse provider probe journal: {error}")))
10298 })
10299 .await
10300 }
10301
10302 pub(crate) async fn advance_provider_probe_journal(
10303 &self,
10304 previous: crate::sync::provider::ProviderProbeJournalRecord,
10305 next: crate::sync::provider::ProviderProbeJournalRecord,
10306 ) -> Result<(), DbError> {
10307 previous.validate_transition(&next).map_err(|error| {
10308 DbError::Message(format!("invalid provider probe journal advance: {error}"))
10309 })?;
10310 let key = format!(
10311 "provider_probe/{}",
10312 hex::encode(previous.probe_id().as_bytes())
10313 );
10314 let previous = serde_json::to_string(&previous).map_err(|error| {
10315 DbError::Message(format!("serialize provider probe journal: {error}"))
10316 })?;
10317 let next = serde_json::to_string(&next).map_err(|error| {
10318 DbError::Message(format!("serialize provider probe journal: {error}"))
10319 })?;
10320 self.call(move |conn| {
10321 let changed = conn
10322 .execute(
10323 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
10324 (&next, &key, &previous),
10325 )
10326 .map_err(DbError::from)?;
10327 if changed != 1 {
10328 return Err(DbError::Message(
10329 "provider probe journal advance lost its exact predecessor".to_string(),
10330 ));
10331 }
10332 Ok(())
10333 })
10334 .await
10335 }
10336
10337 pub(crate) async fn prepare_device_join_challenge_publication(
10338 &self,
10339 challenge: crate::sync::provider::CrossPrincipalProbeChallenge,
10340 ) -> Result<crate::sync::provider::DeviceJoinChallengePublicationRecord, DbError> {
10341 use crate::sync::provider::{
10342 DeviceJoinChallengePublicationProgress, DeviceJoinChallengePublicationRecord,
10343 };
10344
10345 let key = format!(
10346 "device_join_challenge_publication/{}",
10347 hex::encode(challenge.probe_id.as_bytes())
10348 );
10349 let prepared = DeviceJoinChallengePublicationRecord {
10350 challenge,
10351 progress: DeviceJoinChallengePublicationProgress::Prepared,
10352 };
10353 let value = serde_json::to_string(&prepared).map_err(|error| {
10354 DbError::Message(format!(
10355 "serialize device join challenge publication: {error}"
10356 ))
10357 })?;
10358 self.call(move |conn| {
10359 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10360 tx.execute(
10361 "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
10362 (&key, &value),
10363 )
10364 .map_err(DbError::from)?;
10365 let actual: String = tx
10366 .query_row(
10367 "SELECT value FROM protocol_state WHERE key = ?1",
10368 [&key],
10369 |row| row.get(0),
10370 )
10371 .map_err(DbError::from)?;
10372 tx.commit().map_err(DbError::from)?;
10373 let actual: DeviceJoinChallengePublicationRecord = serde_json::from_str(&actual)
10374 .map_err(|error| {
10375 DbError::Message(format!("parse device join challenge publication: {error}"))
10376 })?;
10377 if actual.challenge != prepared.challenge {
10378 return Err(DbError::Message(
10379 "device join challenge probe id was reused with different bytes".to_string(),
10380 ));
10381 }
10382 Ok(actual)
10383 })
10384 .await
10385 }
10386
10387 pub(crate) async fn publish_device_join_challenge(
10388 &self,
10389 authorization: crate::sync::provider::DeviceJoinChallengePublicationAuthorization,
10390 challenge: crate::sync::provider::CrossPrincipalProbeChallenge,
10391 ) -> Result<(), DbError> {
10392 use crate::sync::provider::{
10393 DeviceJoinChallengePublicationProgress, DeviceJoinChallengePublicationRecord,
10394 };
10395
10396 let key = format!(
10397 "device_join_challenge_publication/{}",
10398 hex::encode(challenge.probe_id.as_bytes())
10399 );
10400 self.call(move |conn| {
10401 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10402 let previous_json: String = tx
10403 .query_row(
10404 "SELECT value FROM protocol_state WHERE key = ?1",
10405 [&key],
10406 |row| row.get(0),
10407 )
10408 .map_err(DbError::from)?;
10409 let previous: DeviceJoinChallengePublicationRecord =
10410 serde_json::from_str(&previous_json).map_err(|error| {
10411 DbError::Message(format!("parse device join challenge publication: {error}"))
10412 })?;
10413 if previous.challenge != challenge {
10414 return Err(DbError::Message(
10415 "device join challenge publication differs from prepared bytes".to_string(),
10416 ));
10417 }
10418 match &previous.progress {
10419 DeviceJoinChallengePublicationProgress::Prepared => {
10420 let next = DeviceJoinChallengePublicationRecord {
10421 challenge,
10422 progress: DeviceJoinChallengePublicationProgress::Published {
10423 authorization,
10424 },
10425 };
10426 let next_json = serde_json::to_string(&next).map_err(|error| {
10427 DbError::Message(format!(
10428 "serialize device join challenge publication: {error}"
10429 ))
10430 })?;
10431 let changed = tx
10432 .execute(
10433 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
10434 (&next_json, &key, &previous_json),
10435 )
10436 .map_err(DbError::from)?;
10437 if changed != 1 {
10438 return Err(DbError::Message(
10439 "device join challenge publication lost its exact predecessor"
10440 .to_string(),
10441 ));
10442 }
10443 }
10444 DeviceJoinChallengePublicationProgress::Published {
10445 authorization: existing,
10446 } if existing == &authorization => {}
10447 DeviceJoinChallengePublicationProgress::Published { .. } => {
10448 return Err(DbError::Message(
10449 "device join challenge publication authorization changed".to_string(),
10450 ));
10451 }
10452 DeviceJoinChallengePublicationProgress::ProducerClosed { .. }
10453 | DeviceJoinChallengePublicationProgress::CancelledBeforeCreate { .. } => {
10454 return Err(DbError::Message(
10455 "device join challenge producer is closed".to_string(),
10456 ));
10457 }
10458 }
10459 tx.commit().map_err(DbError::from)
10460 })
10461 .await
10462 }
10463
10464 pub(crate) async fn close_published_device_join_challenge(
10465 &self,
10466 authorization: crate::sync::provider::DeviceJoinChallengePublicationAuthorization,
10467 challenge: crate::sync::provider::CrossPrincipalProbeChallenge,
10468 ) -> Result<(), DbError> {
10469 use crate::sync::provider::{
10470 DeviceJoinChallengePublicationProgress, DeviceJoinChallengePublicationRecord,
10471 };
10472
10473 let key = format!(
10474 "device_join_challenge_publication/{}",
10475 hex::encode(challenge.probe_id.as_bytes())
10476 );
10477 self.call(move |conn| {
10478 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10479 let previous_json: String = tx
10480 .query_row(
10481 "SELECT value FROM protocol_state WHERE key = ?1",
10482 [&key],
10483 |row| row.get(0),
10484 )
10485 .map_err(DbError::from)?;
10486 let previous: DeviceJoinChallengePublicationRecord =
10487 serde_json::from_str(&previous_json).map_err(|error| {
10488 DbError::Message(format!("parse device join challenge publication: {error}"))
10489 })?;
10490 if previous.challenge != challenge {
10491 return Err(DbError::Message(
10492 "device join challenge closure differs from prepared bytes".to_string(),
10493 ));
10494 }
10495 match &previous.progress {
10496 DeviceJoinChallengePublicationProgress::Published {
10497 authorization: existing,
10498 } if existing == &authorization => {
10499 let next = DeviceJoinChallengePublicationRecord {
10500 challenge,
10501 progress: DeviceJoinChallengePublicationProgress::ProducerClosed {
10502 authorization,
10503 },
10504 };
10505 let next_json = serde_json::to_string(&next).map_err(|error| {
10506 DbError::Message(format!(
10507 "serialize device join challenge closure: {error}"
10508 ))
10509 })?;
10510 let changed = tx
10511 .execute(
10512 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
10513 (&next_json, &key, &previous_json),
10514 )
10515 .map_err(DbError::from)?;
10516 if changed != 1 {
10517 return Err(DbError::Message(
10518 "device join challenge closure lost its exact predecessor".to_string(),
10519 ));
10520 }
10521 }
10522 DeviceJoinChallengePublicationProgress::ProducerClosed {
10523 authorization: existing,
10524 } if existing == &authorization => {}
10525 _ => {
10526 return Err(DbError::Message(
10527 "device join challenge cannot close from its current state".to_string(),
10528 ));
10529 }
10530 }
10531 tx.commit().map_err(DbError::from)
10532 })
10533 .await
10534 }
10535
10536 pub(crate) async fn cancel_unpublished_device_join_challenge(
10537 &self,
10538 authorization: crate::sync::provider::DeviceJoinChallengePublicationAuthorization,
10539 challenge: crate::sync::provider::CrossPrincipalProbeChallenge,
10540 cancellation: crate::sync::store_commit::DeviceJoinOutcomeRef,
10541 ) -> Result<(), DbError> {
10542 use crate::sync::provider::{
10543 DeviceJoinChallengePublicationProgress, DeviceJoinChallengePublicationRecord,
10544 };
10545
10546 if cancellation.attempt() != &authorization.attempt {
10547 return Err(DbError::Message(
10548 "device join challenge cancellation names another attempt".to_string(),
10549 ));
10550 }
10551 let key = format!(
10552 "device_join_challenge_publication/{}",
10553 hex::encode(challenge.probe_id.as_bytes())
10554 );
10555 self.call(move |conn| {
10556 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10557 let previous_json: String = tx
10558 .query_row(
10559 "SELECT value FROM protocol_state WHERE key = ?1",
10560 [&key],
10561 |row| row.get(0),
10562 )
10563 .map_err(DbError::from)?;
10564 let previous: DeviceJoinChallengePublicationRecord =
10565 serde_json::from_str(&previous_json).map_err(|error| {
10566 DbError::Message(format!("parse device join challenge publication: {error}"))
10567 })?;
10568 if previous.challenge != challenge {
10569 return Err(DbError::Message(
10570 "device join challenge cancellation differs from prepared bytes".to_string(),
10571 ));
10572 }
10573 match &previous.progress {
10574 DeviceJoinChallengePublicationProgress::Prepared => {
10575 let next = DeviceJoinChallengePublicationRecord {
10576 challenge,
10577 progress: DeviceJoinChallengePublicationProgress::CancelledBeforeCreate {
10578 authorization,
10579 cancellation,
10580 },
10581 };
10582 let next_json = serde_json::to_string(&next).map_err(|error| {
10583 DbError::Message(format!(
10584 "serialize device join challenge cancellation: {error}"
10585 ))
10586 })?;
10587 let changed = tx
10588 .execute(
10589 "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
10590 (&next_json, &key, &previous_json),
10591 )
10592 .map_err(DbError::from)?;
10593 if changed != 1 {
10594 return Err(DbError::Message(
10595 "device join challenge cancellation lost its exact predecessor"
10596 .to_string(),
10597 ));
10598 }
10599 }
10600 DeviceJoinChallengePublicationProgress::CancelledBeforeCreate {
10601 authorization: existing_authorization,
10602 cancellation: existing_cancellation,
10603 } if existing_authorization == &authorization
10604 && existing_cancellation == &cancellation => {}
10605 _ => {
10606 return Err(DbError::Message(
10607 "device join challenge cannot cancel before create from its current state"
10608 .to_string(),
10609 ));
10610 }
10611 }
10612 tx.commit().map_err(DbError::from)
10613 })
10614 .await
10615 }
10616
10617 pub async fn set_protocol_state(&self, key: &str, value: &str) -> Result<(), DbError> {
10618 let (key, value) = (key.to_string(), value.to_string());
10619 self.call(move |conn| {
10620 conn.execute(
10621 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
10622 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
10623 (key, value),
10624 )
10625 .map(|_| ())
10626 .map_err(DbError::from)
10627 })
10628 .await
10629 }
10630
10631 pub async fn delete_protocol_state(&self, key: &str) -> Result<(), DbError> {
10632 let key = key.to_string();
10633 self.call(move |conn| {
10634 conn.execute("DELETE FROM protocol_state WHERE key = ?1", [key])
10635 .map(|_| ())
10636 .map_err(DbError::from)
10637 })
10638 .await
10639 }
10640
10641 pub async fn get_cache_budget(&self, namespace: &str) -> Result<Option<u64>, DbError> {
10651 let key = crate::blob::cache::cache_budget_state_key(namespace);
10652 match self.get_protocol_state(&key).await? {
10653 Some(raw) => raw.parse::<u64>().map(Some).map_err(|e| {
10654 DbError::Message(format!(
10655 "cache budget for {namespace:?} in protocol_state is not a byte count: {e}"
10656 ))
10657 }),
10658 None => Ok(None),
10659 }
10660 }
10661
10662 pub async fn set_cache_budget(&self, namespace: &str, max_bytes: u64) -> Result<(), DbError> {
10668 let key = crate::blob::cache::cache_budget_state_key(namespace);
10669 self.set_protocol_state(&key, &max_bytes.to_string()).await
10670 }
10671
10672 pub(crate) async fn insert_circle_operation(
10675 &self,
10676 journal: crate::sync::circle_ops::CircleOperationJournal,
10677 ) -> Result<(), DbError> {
10678 let row = PreparedCircleOperationRow::from_journal(journal)?;
10679 self.call(move |conn| {
10680 conn.execute(
10681 "INSERT INTO circle_operations (operation_id, circle_id, payload)
10682 VALUES (?1, ?2, ?3)",
10683 rusqlite::params![row.operation_id, row.circle_id, row.payload],
10684 )
10685 .map(|_| ())
10686 .map_err(DbError::from)
10687 })
10688 .await
10689 }
10690
10691 pub(crate) async fn circle_operation(
10692 &self,
10693 circle_id: crate::sync::circle::CircleId,
10694 ) -> Result<Option<crate::sync::circle_ops::CircleOperationJournal>, DbError> {
10695 let circle_id = circle_id.to_string();
10696 self.call(move |conn| {
10697 conn.query_row(
10698 "SELECT payload FROM circle_operations WHERE circle_id = ?1",
10699 [circle_id],
10700 |row| row.get::<_, Vec<u8>>(0),
10701 )
10702 .optional()
10703 .map_err(DbError::from)?
10704 .map(|payload| {
10705 serde_json::from_slice(&payload).map_err(|error| {
10706 DbError::Message(format!("parse circle operation journal: {error}"))
10707 })
10708 })
10709 .transpose()
10710 })
10711 .await
10712 }
10713
10714 pub(crate) async fn oldest_pending_circle_operation(
10715 &self,
10716 ) -> Result<Option<crate::sync::circle_ops::CircleOperationJournal>, DbError> {
10717 self.call(|conn| {
10718 let mut statement = conn
10719 .prepare("SELECT payload FROM circle_operations ORDER BY rowid")
10720 .map_err(DbError::from)?;
10721 let rows = statement
10722 .query_map([], |row| row.get::<_, Vec<u8>>(0))
10723 .map_err(DbError::from)?;
10724 for payload in rows {
10725 let payload = payload.map_err(DbError::from)?;
10726 let journal: crate::sync::circle_ops::CircleOperationJournal =
10727 serde_json::from_slice(&payload).map_err(|error| {
10728 DbError::Message(format!("parse circle operation journal: {error}"))
10729 })?;
10730 if matches!(
10731 journal.status,
10732 crate::sync::circle::CircleOperationState::Pending
10733 ) {
10734 return Ok(Some(journal));
10735 }
10736 }
10737 Ok(None)
10738 })
10739 .await
10740 }
10741
10742 #[doc(hidden)]
10743 pub async fn discard_blocked_circle_operation(
10744 &self,
10745 circle_id: crate::sync::circle::CircleId,
10746 ) -> Result<(), DbError> {
10747 let circle_id = circle_id.to_string();
10748 self.call(move |conn| {
10749 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
10750 let payload = tx
10751 .query_row(
10752 "SELECT payload FROM circle_operations WHERE circle_id = ?1",
10753 [&circle_id],
10754 |row| row.get::<_, Vec<u8>>(0),
10755 )
10756 .optional()
10757 .map_err(DbError::from)?;
10758 let Some(payload) = payload else {
10759 tx.commit().map_err(DbError::from)?;
10760 return Ok(());
10761 };
10762 let journal: crate::sync::circle_ops::CircleOperationJournal =
10763 serde_json::from_slice(&payload).map_err(|error| {
10764 DbError::Message(format!("parse circle operation journal: {error}"))
10765 })?;
10766 match journal.status {
10767 crate::sync::circle::CircleOperationState::Pending => {
10768 return Err(DbError::Message(format!(
10769 "pending circle operation {circle_id} cannot be discarded"
10770 )));
10771 }
10772 crate::sync::circle::CircleOperationState::Blocked { .. } => {}
10773 }
10774 let deleted = tx
10775 .execute(
10776 "DELETE FROM circle_operations WHERE circle_id = ?1",
10777 [&circle_id],
10778 )
10779 .map_err(DbError::from)?;
10780 if deleted != 1 {
10781 return Err(DbError::Message(format!(
10782 "blocked circle operation {circle_id} disappeared during discard"
10783 )));
10784 }
10785 tx.commit().map_err(DbError::from)
10786 })
10787 .await
10788 }
10789
10790 pub async fn get_circle_operations(
10791 &self,
10792 ) -> Result<Vec<crate::sync::circle::CircleOperationInfo>, DbError> {
10793 self.call(|conn| {
10794 let mut statement = conn
10795 .prepare("SELECT payload FROM circle_operations ORDER BY rowid")
10796 .map_err(DbError::from)?;
10797 let operations = statement
10798 .query_map([], |row| row.get::<_, Vec<u8>>(0))
10799 .map_err(DbError::from)?
10800 .map(|payload| {
10801 let payload = payload.map_err(DbError::from)?;
10802 let journal: crate::sync::circle_ops::CircleOperationJournal =
10803 serde_json::from_slice(&payload).map_err(|error| {
10804 DbError::Message(format!("parse circle operation journal: {error}"))
10805 })?;
10806 let circle_id = journal.circle_id();
10807 Ok(crate::sync::circle::CircleOperationInfo {
10808 circle_id,
10809 name: journal.creation.metadata.name,
10810 state: journal.status,
10811 })
10812 })
10813 .collect();
10814 operations
10815 })
10816 .await
10817 }
10818
10819 pub async fn get_circles(
10820 &self,
10821 identity_pubkey: &str,
10822 ) -> Result<Vec<crate::sync::circle::CircleInfo>, DbError> {
10823 let identity_pubkey = identity_pubkey.to_string();
10824 self.call(move |conn| {
10825 let mut statement = conn
10826 .prepare(
10827 "SELECT activation.circle_id, activation.control_bytes,
10828 metadata.metadata_bytes, roster.roster_bytes
10829 FROM circle_control_activations AS activation
10830 JOIN circle_access_cache AS access
10831 ON access.circle_id = activation.circle_id
10832 AND access.control_coord = activation.control_coord
10833 JOIN circle_metadata_cache AS metadata
10834 ON metadata.circle_id = activation.circle_id
10835 AND metadata.control_coord = activation.control_coord
10836 JOIN circle_roster_cache AS roster
10837 ON roster.circle_id = activation.circle_id
10838 AND roster.control_coord = activation.control_coord
10839 WHERE access.disposition = 'active'
10840 ORDER BY activation.circle_id, activation.seq",
10841 )
10842 .map_err(DbError::from)?;
10843 let rows = statement
10844 .query_map([], |row| {
10845 Ok((
10846 row.get::<_, String>(0)?,
10847 row.get::<_, Vec<u8>>(1)?,
10848 row.get::<_, Vec<u8>>(2)?,
10849 row.get::<_, Vec<u8>>(3)?,
10850 ))
10851 })
10852 .map_err(DbError::from)?;
10853 let mut circles = Vec::new();
10854 for row in rows {
10855 let (circle_id, control_bytes, metadata_bytes, roster_bytes) =
10856 row.map_err(DbError::from)?;
10857 let circle_id: crate::sync::circle::CircleId =
10858 circle_id.parse().map_err(|error| {
10859 DbError::Message(format!("parse activated circle id: {error}"))
10860 })?;
10861 let control: crate::sync::circle::CircleControl =
10862 serde_json::from_slice(&control_bytes).map_err(|error| {
10863 DbError::Message(format!("parse activated circle control: {error}"))
10864 })?;
10865 let metadata: crate::sync::circle::CircleMetadata =
10866 serde_json::from_slice(&metadata_bytes).map_err(|error| {
10867 DbError::Message(format!("parse activated circle metadata: {error}"))
10868 })?;
10869 let roster: crate::sync::circle::CircleMaterializedRoster =
10870 serde_json::from_slice(&roster_bytes).map_err(|error| {
10871 DbError::Message(format!("parse activated circle roster: {error}"))
10872 })?;
10873 if !control.verify()
10874 || !metadata.verify()
10875 || !roster.verify()
10876 || control.circle_id != circle_id
10877 || metadata.circle_id != circle_id
10878 || match &control.metadata {
10879 crate::sync::circle::CircleMetadataStateRef::MergeConcurrent {
10880 selected,
10881 state_hash,
10882 ..
10883 } => {
10884 selected != &metadata.coord() || *state_hash != metadata.metadata_hash()
10885 }
10886 crate::sync::circle::CircleMetadataStateRef::Serial { current } => {
10887 current != &metadata.coord()
10888 }
10889 }
10890 || match &control.roster {
10891 crate::sync::circle::CircleRosterStateRef::MergeConcurrent {
10892 state_hash,
10893 ..
10894 }
10895 | crate::sync::circle::CircleRosterStateRef::Serial { state_hash } => {
10896 *state_hash != roster.state_hash()
10897 }
10898 }
10899 {
10900 return Err(DbError::Message(format!(
10901 "activated circle {circle_id} has inconsistent cached state"
10902 )));
10903 }
10904 let role = roster
10905 .members()
10906 .get(&identity_pubkey)
10907 .copied()
10908 .ok_or_else(|| {
10909 DbError::Message(format!(
10910 "activated circle {circle_id} excludes the local identity"
10911 ))
10912 })?;
10913 circles.push(crate::sync::circle::CircleInfo {
10914 id: circle_id,
10915 name: metadata.name,
10916 role,
10917 });
10918 }
10919 Ok(circles)
10920 })
10921 .await
10922 }
10923
10924 pub(crate) async fn circle_publication_context(
10925 &self,
10926 circle_id: crate::sync::circle::CircleId,
10927 expected_control: crate::sync::circle::CircleControlCoord,
10928 ) -> Result<(EncryptionService, crate::KeyFingerprint), DbError> {
10929 self.call(move |conn| {
10930 let control_coord = serde_json::to_string(&expected_control).map_err(|error| {
10931 DbError::Message(format!("serialize Circle publication coordinate: {error}"))
10932 })?;
10933 let (control_bytes, access_bytes): (Vec<u8>, Vec<u8>) = conn
10934 .query_row(
10935 "SELECT activation.control_bytes, access.access_bytes
10936 FROM circle_control_activations AS activation
10937 JOIN circle_access_cache AS access
10938 ON access.circle_id = activation.circle_id
10939 AND access.control_coord = activation.control_coord
10940 WHERE activation.circle_id = ?1
10941 AND activation.control_coord = ?2
10942 AND access.disposition = 'active'",
10943 (circle_id.to_string(), control_coord),
10944 |row| Ok((row.get(0)?, row.get(1)?)),
10945 )
10946 .map_err(DbError::from)?;
10947 let control: crate::sync::circle::CircleControl =
10948 serde_json::from_slice(&control_bytes).map_err(|error| {
10949 DbError::Message(format!("parse Circle publication control: {error}"))
10950 })?;
10951 let access: crate::sync::circle::CircleAccessLeaf =
10952 serde_json::from_slice(&access_bytes).map_err(|error| {
10953 DbError::Message(format!("parse Circle publication access: {error}"))
10954 })?;
10955 if !control.verify()
10956 || control.circle_id != circle_id
10957 || control.coord() != expected_control
10958 || !access.verify_signature()
10959 || access.circle_id != circle_id
10960 || access.epoch_id != control.epoch_id
10961 {
10962 return Err(DbError::Message(format!(
10963 "Circle {circle_id} publication authority differs from its exact activation"
10964 )));
10965 }
10966 let crate::sync::circle::CircleAccessDisposition::Active {
10967 keyring,
10968 key_fingerprint,
10969 roster,
10970 } = access.disposition
10971 else {
10972 return Err(DbError::Message(format!(
10973 "Circle {circle_id} has no active publication key"
10974 )));
10975 };
10976 if key_fingerprint != control.key_fingerprint || roster != control.roster {
10977 return Err(DbError::Message(format!(
10978 "Circle {circle_id} publication key differs from its exact control"
10979 )));
10980 }
10981 let encryption = EncryptionService::from(
10982 crate::encryption::MasterKeyring::from_serialized(&keyring).map_err(|error| {
10983 DbError::Message(format!("parse Circle publication keyring: {error}"))
10984 })?,
10985 );
10986 if encryption.seal_key_fingerprint() != key_fingerprint {
10987 return Err(DbError::Message(format!(
10988 "Circle {circle_id} publication key fingerprint is invalid"
10989 )));
10990 }
10991 Ok((encryption, key_fingerprint))
10992 })
10993 .await
10994 }
10995
10996 pub(crate) async fn update_circle_operation(
10997 &self,
10998 journal: crate::sync::circle_ops::CircleOperationJournal,
10999 ) -> Result<(), DbError> {
11000 let row = PreparedCircleOperationRow::from_journal(journal)?;
11001 self.call(move |conn| {
11002 let updated = conn
11003 .execute(
11004 "UPDATE circle_operations SET payload = ?3
11005 WHERE operation_id = ?1 AND circle_id = ?2",
11006 rusqlite::params![row.operation_id, row.circle_id, row.payload],
11007 )
11008 .map_err(DbError::from)?;
11009 if updated != 1 {
11010 return Err(DbError::Message(
11011 "circle operation disappeared during publication".to_string(),
11012 ));
11013 }
11014 Ok(())
11015 })
11016 .await
11017 }
11018
11019 pub(crate) async fn block_circle_operation(
11020 &self,
11021 circle_id: crate::sync::circle::CircleId,
11022 reason: String,
11023 ) -> Result<(), DbError> {
11024 let mut journal = self
11025 .circle_operation(circle_id)
11026 .await?
11027 .ok_or_else(|| DbError::Message(format!("circle operation {circle_id} is absent")))?;
11028 journal.status = crate::sync::circle::CircleOperationState::Blocked { reason };
11029 self.update_circle_operation(journal).await
11030 }
11031
11032 pub(crate) async fn activate_circle_operation(
11033 &self,
11034 journal: crate::sync::circle_ops::CircleOperationJournal,
11035 identity: &crate::keys::UserKeypair,
11036 ) -> Result<(), DbError> {
11037 let identity = identity.clone();
11038 self.call(move |conn| {
11039 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
11040 if !matches!(
11041 journal.status,
11042 crate::sync::circle::CircleOperationState::Pending
11043 ) {
11044 return Err(DbError::Message(
11045 "blocked circle operation cannot activate".to_string(),
11046 ));
11047 }
11048 let creation = &journal.creation;
11049 let resolved_roster = creation.resolved_roster();
11050 if !creation.control.verify()
11051 || !creation.metadata.verify()
11052 || !resolved_roster.verify()
11053 {
11054 return Err(DbError::Message(
11055 "circle operation contains invalid signed objects".to_string(),
11056 ));
11057 }
11058 let unverified_commit: StoreBatchCommit = serde_json::from_slice(&journal.commit_bytes)
11059 .map_err(|error| DbError::Message(format!("parse circle Store commit: {error}")))?;
11060 let root = required_store_root_authority_on(&tx)?;
11061 let author =
11062 load_activated_registration_on(&tx, &root, &unverified_commit.author_registration)?;
11063 let verify_commit = || {
11064 let commit = StoreBatchCommit::parse_at(
11065 &journal.commit_bytes,
11066 root.store_root_hash,
11067 &journal.commit_ref.coord,
11068 &author,
11069 )
11070 .map_err(|error| {
11071 DbError::Message(format!("verify circle Store commit: {error}"))
11072 })?;
11073 journal
11074 .commit_ref
11075 .verify_commit(&commit)
11076 .map_err(|error| DbError::Message(error.to_string()))?;
11077 if journal.commit_ref.object.slot().logical_key()
11078 != commit_semantic_prefix(
11079 commit.candidate_family(),
11080 &match &journal.commit_ref.coord {
11081 StoreCommitCoord::MergeConcurrent { stream_id, .. } => {
11082 stream_id.to_string()
11083 }
11084 StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
11085 },
11086 commit.seq(),
11087 commit.commit_hash(),
11088 ) + ".json"
11089 {
11090 return Err(DbError::Message(
11091 "circle commit exact object occupies a different semantic slot".to_string(),
11092 ));
11093 }
11094 let [control_ref] = commit.circle_controls() else {
11095 return Err(DbError::Message(
11096 "circle creation Store commit is not an exact control-only batch"
11097 .to_string(),
11098 ));
11099 };
11100 let expected_ref = creation.control_ref(control_ref.objects().clone());
11101 if control_ref != &expected_ref
11102 || commit.store_package().is_some()
11103 || !commit.circle_packages().is_empty()
11104 || commit.control().is_some()
11105 || !commit.device_registrations().is_empty()
11106 {
11107 return Err(DbError::Message(
11108 "circle creation Store commit is not an exact control-only batch"
11109 .to_string(),
11110 ));
11111 }
11112 let activation = crate::sync::circle_ops::verify_local_circle_activation(
11113 &journal,
11114 &journal.commit_ref,
11115 &commit,
11116 &author,
11117 &identity,
11118 )
11119 .map_err(|error| DbError::Message(error.to_string()))?;
11120 Ok((commit, activation))
11121 };
11122 let (commit, activation) = match &journal.policy {
11123 crate::sync::circle_ops::CircleOperationPolicy::MergeConcurrent { head } => {
11124 let (commit, activation) = verify_commit()?;
11125 let parsed = StoreDeviceHead::parse_at(
11126 &head.to_bytes(),
11127 commit.store_root_hash,
11128 &author,
11129 &journal.commit_ref,
11130 )
11131 .map_err(|error| {
11132 DbError::Message(format!("verify circle activation head: {error}"))
11133 })?;
11134 if parsed.commit != journal.commit_ref {
11135 return Err(DbError::Message(
11136 "circle activation head names a different commit".to_string(),
11137 ));
11138 }
11139 Self::record_materialized_commit_on(&tx, &commit, &journal.commit_ref)?;
11140 (commit, activation)
11141 }
11142 crate::sync::circle_ops::CircleOperationPolicy::Serial {
11143 head,
11144 authorization,
11145 ..
11146 } => {
11147 let (commit, activation) = verify_commit()?;
11148 let parsed =
11149 StoreSerialHead::parse(&head.to_bytes(), commit.store_root_hash, &author)
11150 .map_err(|error| {
11151 DbError::Message(format!("verify circle Serial head: {error}"))
11152 })?;
11153 if !matches!(
11154 parsed.state,
11155 StoreSerialHeadState::Commit { ref commit, .. }
11156 if commit == &journal.commit_ref
11157 ) {
11158 return Err(DbError::Message(
11159 "circle Serial head names a different commit".to_string(),
11160 ));
11161 }
11162 Self::record_materialized_serial_commit_on(
11163 &tx,
11164 &commit,
11165 &journal.commit_ref,
11166 authorization,
11167 )?;
11168 (commit, activation)
11169 }
11170 };
11171 Self::record_verified_circle_activations_on(
11172 &tx,
11173 &commit,
11174 &journal.commit_ref,
11175 &[activation],
11176 )?;
11177 let deleted = tx
11178 .execute(
11179 "DELETE FROM circle_operations WHERE operation_id = ?1 AND circle_id = ?2",
11180 rusqlite::params![journal.operation_id, creation.circle_id.to_string()],
11181 )
11182 .map_err(DbError::from)?;
11183 if deleted != 1 {
11184 return Err(DbError::Message(
11185 "circle operation disappeared during activation".to_string(),
11186 ));
11187 }
11188 tx.commit().map_err(DbError::from)
11189 })
11190 .await
11191 }
11192
11193 pub(crate) async fn materialized_frontier(
11194 &self,
11195 ) -> Result<BTreeMap<String, StoreBatchCommitRef>, DbError> {
11196 self.call(|conn| Self::materialized_frontier_on(conn, None))
11197 .await
11198 }
11199
11200 pub(crate) async fn exact_materialized_ref(
11201 &self,
11202 stream_id: &str,
11203 sequence: u64,
11204 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
11205 let stream_id = stream_id.to_string();
11206 self.call(move |conn| Self::materialized_commit_ref_on(conn, &stream_id, sequence))
11207 .await
11208 }
11209
11210 pub(crate) async fn snapshot_coverage_frontier(&self) -> Result<CommitFrontier, DbError> {
11211 let policy = self.write_policy();
11212 self.call(move |conn| {
11213 let mut stmt = conn
11214 .prepare("SELECT device_id, seq, commit_ref FROM snapshot_coverage")
11215 .map_err(DbError::from)?;
11216 let rows = stmt
11217 .query_map([], |row| {
11218 Ok((
11219 row.get::<_, String>(0)?,
11220 row.get::<_, i64>(1)?,
11221 row.get::<_, String>(2)?,
11222 ))
11223 })
11224 .map_err(DbError::from)?;
11225 let mut frontier = BTreeMap::new();
11226 for row in rows {
11227 let (device_id, seq, reference) = row.map_err(DbError::from)?;
11228 let seq = Self::sequence_from_sqlite(&device_id, seq)?;
11229 let reference = Self::parse_stored_commit_ref(&device_id, seq, &reference)?;
11230 frontier.insert(device_id.clone(), reference);
11231 }
11232 CommitFrontier::from_refs(policy, frontier)
11233 .map_err(|error| DbError::Message(format!("snapshot coverage frontier: {error}")))
11234 })
11235 .await
11236 }
11237
11238 pub(crate) fn materialized_frontier_on(
11239 conn: &Connection,
11240 exclude_device: Option<&str>,
11241 ) -> Result<BTreeMap<String, StoreBatchCommitRef>, DbError> {
11242 let mut frontier = BTreeMap::new();
11243 let mut stmt = conn
11244 .prepare(
11245 "SELECT m.device_id, m.seq, m.commit_ref \
11246 FROM materialized_commits m \
11247 JOIN (SELECT device_id, MAX(seq) AS seq FROM materialized_commits \
11248 GROUP BY device_id) latest \
11249 ON latest.device_id = m.device_id AND latest.seq = m.seq",
11250 )
11251 .map_err(DbError::from)?;
11252 let rows = stmt
11253 .query_map([], |row| {
11254 Ok((
11255 row.get::<_, String>(0)?,
11256 row.get::<_, i64>(1)?,
11257 row.get::<_, String>(2)?,
11258 ))
11259 })
11260 .map_err(DbError::from)?;
11261 for row in rows {
11262 let (device_id, seq, reference) = row.map_err(DbError::from)?;
11263 if exclude_device == Some(device_id.as_str()) {
11264 continue;
11265 }
11266 let seq = Self::sequence_from_sqlite(&device_id, seq)?;
11267 frontier.insert(
11268 device_id.clone(),
11269 Self::parse_stored_commit_ref(&device_id, seq, &reference)?,
11270 );
11271 }
11272
11273 let mut coverage = conn
11274 .prepare("SELECT device_id, seq, commit_ref FROM snapshot_coverage")
11275 .map_err(DbError::from)?;
11276 let rows = coverage
11277 .query_map([], |row| {
11278 Ok((
11279 row.get::<_, String>(0)?,
11280 row.get::<_, i64>(1)?,
11281 row.get::<_, String>(2)?,
11282 ))
11283 })
11284 .map_err(DbError::from)?;
11285 for row in rows {
11286 let (device_id, seq, reference) = row.map_err(DbError::from)?;
11287 if exclude_device == Some(device_id.as_str()) {
11288 continue;
11289 }
11290 let seq = Self::sequence_from_sqlite(&device_id, seq)?;
11291 let reference = Self::parse_stored_commit_ref(&device_id, seq, &reference)?;
11292 if frontier
11293 .get(&device_id)
11294 .is_none_or(|current| current.coord.sequence() < reference.coord.sequence())
11295 {
11296 frontier.insert(device_id, reference);
11297 }
11298 }
11299 Ok(frontier)
11300 }
11301
11302 fn parse_stored_commit_ref(
11303 stream_id: &str,
11304 sequence: u64,
11305 encoded: &str,
11306 ) -> Result<StoreBatchCommitRef, DbError> {
11307 let reference: StoreBatchCommitRef = serde_json::from_str(encoded)
11308 .map_err(|error| DbError::Message(format!("stored exact Store commit ref: {error}")))?;
11309 let coordinate_matches = match &reference.coord {
11310 StoreCommitCoord::Serial { sequence: declared } => {
11311 stream_id == SERIAL_STREAM_ID && *declared == sequence
11312 }
11313 StoreCommitCoord::MergeConcurrent {
11314 stream_id: declared,
11315 sequence: declared_sequence,
11316 } => declared.to_string() == stream_id && *declared_sequence == sequence,
11317 };
11318 if !coordinate_matches {
11319 return Err(DbError::Message(format!(
11320 "stored exact Store commit ref differs from {stream_id}/{sequence}"
11321 )));
11322 }
11323 Ok(reference)
11324 }
11325
11326 pub(crate) fn materialized_commit_ref_on(
11327 conn: &Connection,
11328 stream_id: &str,
11329 sequence: u64,
11330 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
11331 let seq = Self::sequence_to_sqlite(stream_id, sequence)?;
11332 conn.query_row(
11333 "SELECT commit_ref FROM materialized_commits WHERE device_id = ?1 AND seq = ?2",
11334 (stream_id, seq),
11335 |row| row.get::<_, String>(0),
11336 )
11337 .optional()
11338 .map_err(DbError::from)?
11339 .map(|encoded| Self::parse_stored_commit_ref(stream_id, sequence, &encoded))
11340 .transpose()
11341 }
11342
11343 fn latest_position_for_device_on(
11344 conn: &Connection,
11345 device_id: &str,
11346 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
11347 let materialized = conn
11348 .query_row(
11349 "SELECT seq, commit_ref FROM materialized_commits
11350 WHERE device_id = ?1 ORDER BY seq DESC LIMIT 1",
11351 [device_id],
11352 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
11353 )
11354 .optional()
11355 .map_err(DbError::from)?;
11356 let coverage = conn
11357 .query_row(
11358 "SELECT seq, commit_ref FROM snapshot_coverage WHERE device_id = ?1",
11359 [device_id],
11360 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
11361 )
11362 .optional()
11363 .map_err(DbError::from)?;
11364 let references = [materialized, coverage]
11365 .into_iter()
11366 .flatten()
11367 .map(|(seq, reference)| {
11368 let seq = Self::sequence_from_sqlite(device_id, seq)?;
11369 Self::parse_stored_commit_ref(device_id, seq, &reference)
11370 })
11371 .collect::<Result<Vec<_>, DbError>>()?;
11372 if references.len() == 2
11373 && references[0].coord.sequence() == references[1].coord.sequence()
11374 && references[0] != references[1]
11375 {
11376 return Err(DbError::Message(format!(
11377 "materialized ledger and snapshot coverage fork {device_id:?} at sequence {}",
11378 references[0].coord.sequence()
11379 )));
11380 }
11381 Ok(references
11382 .into_iter()
11383 .max_by_key(|reference| reference.coord.sequence()))
11384 }
11385
11386 pub(crate) fn record_activated_store_device_registrations_on(
11387 conn: &Connection,
11388 commit: &StoreBatchCommit,
11389 registrations: &[(
11390 StoreDeviceRegistration,
11391 crate::sync::store_commit::StoreDeviceRegistrationActivation,
11392 )],
11393 ) -> Result<(), DbError> {
11394 if registrations.len() != commit.device_registrations().len() {
11395 return Err(DbError::Message(
11396 "Store device registration activation count differs from the signed commit"
11397 .to_string(),
11398 ));
11399 }
11400 for signed in commit.device_registrations() {
11401 let (registration, authority) = registrations
11402 .iter()
11403 .find(|(registration, _)| registration.device_id == signed.registration.device_id)
11404 .ok_or_else(|| {
11405 DbError::Message(format!(
11406 "Store commit is missing registration bytes for {}",
11407 signed.registration.device_id
11408 ))
11409 })?;
11410 signed
11411 .registration
11412 .verify_registration(registration)
11413 .map_err(|error| DbError::Message(error.to_string()))?;
11414 if registration.store_root.store_root_hash != commit.store_root_hash {
11415 return Err(DbError::Message(format!(
11416 "Store registration {} belongs to a different Store",
11417 registration.device_id
11418 )));
11419 }
11420 let expected_authority = match (®istration.origin, &signed.authority) {
11421 (
11422 crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
11423 attempt_id: origin_attempt,
11424 outcome_slot,
11425 ..
11426 },
11427 crate::sync::store_commit::StoreDeviceRegistrationActivationRef::Join {
11428 attempt_id,
11429 outcome,
11430 },
11431 ) if origin_attempt == attempt_id && outcome_slot == outcome.slot() => {
11432 crate::sync::store_commit::StoreDeviceRegistrationActivation::Join {
11433 attempt_id: *attempt_id,
11434 outcome: outcome.clone(),
11435 }
11436 }
11437 (
11438 crate::sync::store_commit::StoreDeviceRegistrationOrigin::Recovery {
11439 recovery_id: origin_recovery,
11440 recovery_slot,
11441 ..
11442 },
11443 crate::sync::store_commit::StoreDeviceRegistrationActivationRef::Recovery {
11444 recovery_id,
11445 node,
11446 },
11447 ) if origin_recovery == recovery_id && recovery_slot == node.slot() => {
11448 crate::sync::store_commit::StoreDeviceRegistrationActivation::Recovery {
11449 recovery_id: *recovery_id,
11450 node: node.clone(),
11451 }
11452 }
11453 _ => {
11454 return Err(DbError::Message(format!(
11455 "Store registration {} origin differs from its signed activation authority",
11456 registration.device_id
11457 )))
11458 }
11459 };
11460 if authority != &expected_authority {
11461 return Err(DbError::Message(format!(
11462 "verified Store registration {} authority differs from the signed commit",
11463 registration.device_id
11464 )));
11465 }
11466 let registration_bytes = registration.to_bytes();
11467 let registration_object =
11468 serde_json::to_string(&signed.registration).map_err(|error| {
11469 DbError::Message(format!("serialize Store registration exact ref: {error}"))
11470 })?;
11471 let activation_authority = serde_json::to_string(authority).map_err(|error| {
11472 DbError::Message(format!("serialize Store registration authority: {error}"))
11473 })?;
11474 let inserted = conn
11475 .execute(
11476 "INSERT INTO store_device_registration_activations
11477 (device_id, registration_hash, author_pubkey, device_signing_pubkey,
11478 registration_bytes, registration_object, activation_authority)
11479 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
11480 ON CONFLICT(device_id) DO NOTHING",
11481 rusqlite::params![
11482 registration.device_id.to_string(),
11483 signed.registration.registration_hash.to_string(),
11484 registration.author_pubkey,
11485 registration.device_signing_pubkey,
11486 registration_bytes,
11487 registration_object,
11488 activation_authority,
11489 ],
11490 )
11491 .map_err(DbError::from)?;
11492 if inserted == 0 {
11493 let existing: (String, Vec<u8>, String, String) = conn
11494 .query_row(
11495 "SELECT registration_hash, registration_bytes, registration_object,
11496 activation_authority
11497 FROM store_device_registration_activations WHERE device_id = ?1",
11498 [registration.device_id.to_string()],
11499 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
11500 )
11501 .map_err(DbError::from)?;
11502 if existing
11503 != (
11504 signed.registration.registration_hash.to_string(),
11505 registration.to_bytes(),
11506 serde_json::to_string(&signed.registration).map_err(|error| {
11507 DbError::Message(format!(
11508 "serialize Store registration exact ref: {error}"
11509 ))
11510 })?,
11511 serde_json::to_string(authority).map_err(|error| {
11512 DbError::Message(format!(
11513 "serialize Store registration authority: {error}"
11514 ))
11515 })?,
11516 )
11517 {
11518 return Err(DbError::Message(format!(
11519 "Store device {} already has a different one-shot registration",
11520 registration.device_id
11521 )));
11522 }
11523 }
11524 let local: Option<LocalDeviceRegistrationJournalRow> = conn
11525 .query_row(
11526 "SELECT device_id, registration_hash, registration_bytes, prepared_object, \
11527 initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state \
11528 FROM local_store_device_registration WHERE singleton = 1",
11529 [],
11530 |row| {
11531 Ok((
11532 row.get(0)?,
11533 row.get(1)?,
11534 row.get(2)?,
11535 row.get(3)?,
11536 row.get(4)?,
11537 row.get(5)?,
11538 row.get(6)?,
11539 row.get(7)?,
11540 ))
11541 },
11542 )
11543 .optional()
11544 .map_err(DbError::from)?;
11545 let Some((
11546 local_device,
11547 local_hash,
11548 local_bytes,
11549 local_prepared,
11550 local_ack_ref,
11551 local_ack_bytes,
11552 local_ack_prepared,
11553 local_state,
11554 )) = local
11555 else {
11556 continue;
11557 };
11558 if local_device != registration.device_id.to_string() {
11559 continue;
11560 }
11561 let local_prepared: PreparedExactObject = serde_json::from_str(&local_prepared)
11562 .map_err(|error| DbError::Message(format!("local registration object: {error}")))?;
11563 let local_ack_ref: StoreAckRef = serde_json::from_str(&local_ack_ref)
11564 .map_err(|error| DbError::Message(format!("local initial ack ref: {error}")))?;
11565 let local_ack_prepared: PreparedExactObject = serde_json::from_str(&local_ack_prepared)
11566 .map_err(|error| DbError::Message(format!("local initial ack object: {error}")))?;
11567 if local_hash != signed.registration.registration_hash.to_string()
11568 || local_bytes != registration.to_bytes()
11569 || local_prepared.reference() != &signed.registration.object
11570 || local_ack_prepared.reference() != &local_ack_ref.object
11571 {
11572 return Err(DbError::Message(
11573 "activating commit differs from the complete local registration ref"
11574 .to_string(),
11575 ));
11576 }
11577 let ack = StoreAck::parse_at(
11578 &local_ack_bytes,
11579 registration.store_root.store_root_hash,
11580 &local_ack_ref,
11581 registration,
11582 )
11583 .map_err(|error| DbError::Message(format!("local initial ack: {error}")))?;
11584 if ack.revision != 1
11585 || ack.predecessor.is_some()
11586 || local_ack_prepared
11587 .reference()
11588 .verify(local_ack_prepared.stored_bytes())
11589 .is_err()
11590 {
11591 return Err(DbError::Message(
11592 "local registration journal does not carry an initial acknowledgement"
11593 .to_string(),
11594 ));
11595 }
11596 let state: LocalDeviceRegistrationState =
11597 serde_json::from_str(&local_state).map_err(|error| {
11598 DbError::Message(format!("local registration journal: {error}"))
11599 })?;
11600 let activated_state = LocalDeviceRegistrationState::Activated {
11601 authority: authority.clone(),
11602 };
11603 match state {
11604 LocalDeviceRegistrationState::Prepared => {
11605 return Err(DbError::Message(
11606 "Store commit cannot activate a registration before exact creation"
11607 .to_string(),
11608 ));
11609 }
11610 LocalDeviceRegistrationState::Created => {
11611 let updated = conn
11612 .execute(
11613 "UPDATE local_store_device_registration SET state = ?1 \
11614 WHERE singleton = 1 AND device_id = ?2 AND registration_hash = ?3 \
11615 AND initial_ack_ref = ?4 AND state = ?5",
11616 rusqlite::params![
11617 serde_json::to_string(&activated_state).map_err(|error| {
11618 DbError::Message(format!(
11619 "serialize activated journal: {error}"
11620 ))
11621 })?,
11622 local_device,
11623 local_hash,
11624 serde_json::to_string(&local_ack_ref).map_err(|error| {
11625 DbError::Message(format!(
11626 "serialize local initial ack: {error}"
11627 ))
11628 })?,
11629 serde_json::to_string(&LocalDeviceRegistrationState::Created)
11630 .map_err(|error| DbError::Message(format!(
11631 "serialize created journal: {error}"
11632 )))?,
11633 ],
11634 )
11635 .map_err(DbError::from)?;
11636 if updated != 1 {
11637 return Err(DbError::Message(
11638 "local registration journal changed during activation".to_string(),
11639 ));
11640 }
11641 conn.execute(
11642 "INSERT INTO published_store_acks (singleton, ack_ref, successor_slot) \
11643 VALUES (1, ?1, ?2)",
11644 rusqlite::params![
11645 serde_json::to_string(&local_ack_ref).map_err(|error| {
11646 DbError::Message(format!(
11647 "serialize activated initial ack: {error}"
11648 ))
11649 })?,
11650 serde_json::to_string(&ack.successor.next_slot).map_err(|error| {
11651 DbError::Message(format!(
11652 "serialize activated ack successor: {error}"
11653 ))
11654 })?,
11655 ],
11656 )
11657 .map_err(DbError::from)?;
11658 conn.execute(
11659 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
11660 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
11661 (
11662 LOCAL_DEVICE_ID_STATE_KEY,
11663 registration.device_id.to_string(),
11664 ),
11665 )
11666 .map_err(DbError::from)?;
11667 }
11668 LocalDeviceRegistrationState::Activated {
11669 authority: existing,
11670 } if existing == *authority => {
11671 let stored_ack: (String, String) = conn
11672 .query_row(
11673 "SELECT ack_ref, successor_slot FROM published_store_acks \
11674 WHERE singleton = 1",
11675 [],
11676 |row| Ok((row.get(0)?, row.get(1)?)),
11677 )
11678 .map_err(DbError::from)?;
11679 let local_device_id: Option<String> = conn
11680 .query_row(
11681 "SELECT value FROM protocol_state WHERE key = ?1",
11682 [LOCAL_DEVICE_ID_STATE_KEY],
11683 |row| row.get(0),
11684 )
11685 .optional()
11686 .map_err(DbError::from)?;
11687 if stored_ack.0
11688 != serde_json::to_string(&local_ack_ref).map_err(|error| {
11689 DbError::Message(format!("serialize replayed initial ack: {error}"))
11690 })?
11691 || stored_ack.1
11692 != serde_json::to_string(&ack.successor.next_slot).map_err(|error| {
11693 DbError::Message(format!(
11694 "serialize replayed ack successor: {error}"
11695 ))
11696 })?
11697 || local_device_id.as_deref() != Some(local_device.as_str())
11698 {
11699 return Err(DbError::Message(
11700 "activated local journal differs from its exact initial ack"
11701 .to_string(),
11702 ));
11703 }
11704 }
11705 LocalDeviceRegistrationState::Activated { .. } => {
11706 return Err(DbError::Message(
11707 "local registration already has another exact activation authority"
11708 .to_string(),
11709 ));
11710 }
11711 LocalDeviceRegistrationState::Retired { .. } => {
11712 return Err(DbError::Message(
11713 "retired local registration cannot be activated again".to_string(),
11714 ));
11715 }
11716 }
11717 }
11718 Ok(())
11719 }
11720
11721 #[cfg(test)]
11722 pub(crate) async fn activated_store_device_registrations(
11723 &self,
11724 ) -> Result<Vec<StoreDeviceRegistration>, DbError> {
11725 Ok(self
11726 .activated_store_device_registration_records()
11727 .await?
11728 .into_iter()
11729 .map(|(_, registration)| registration)
11730 .collect())
11731 }
11732
11733 pub(crate) async fn store_device_state_for_order(
11734 &self,
11735 order: &crate::sync::store_commit::StoreCommitOrder,
11736 ) -> Result<(StoreDeviceStateRef, ResolvedStoreDeviceState), DbError> {
11737 let order = order.clone();
11738 self.call(move |conn| {
11739 let genesis = load_store_device_genesis_state_on(conn)?;
11740 match order {
11741 crate::sync::store_commit::StoreCommitOrder::MergeConcurrent {
11742 predecessor,
11743 dependencies,
11744 ..
11745 } => {
11746 let mut frontier = dependencies;
11747 if let Some(predecessor) = predecessor {
11748 let StoreCommitCoord::MergeConcurrent { stream_id, .. } = predecessor.coord
11749 else {
11750 return Err(DbError::Message(
11751 "Merge predecessor has a Serial coordinate".to_string(),
11752 ));
11753 };
11754 if frontier
11755 .insert(stream_id, predecessor.clone())
11756 .is_some_and(|current| current != predecessor)
11757 {
11758 return Err(DbError::Message(
11759 "Merge predecessor differs from the same-stream dependency"
11760 .to_string(),
11761 ));
11762 }
11763 }
11764 let state = if frontier.is_empty() {
11765 genesis
11766 } else {
11767 ResolvedStoreDeviceState::merge(
11768 frontier
11769 .values()
11770 .map(|reference| load_store_device_snapshot_on(conn, reference))
11771 .collect::<Result<Vec<_>, _>>()?,
11772 )
11773 .map_err(|error| DbError::Message(error.to_string()))?
11774 };
11775 let reference = StoreDeviceStateRef::merge_concurrent(
11776 CommitFrontier::MergeConcurrent(frontier),
11777 &state,
11778 )
11779 .map_err(|error| DbError::Message(error.to_string()))?;
11780 Ok((reference, state))
11781 }
11782 crate::sync::store_commit::StoreCommitOrder::Serial { predecessor, .. } => {
11783 let state = match &predecessor {
11784 StoreSerialPredecessor::Genesis { .. } => genesis,
11785 StoreSerialPredecessor::Commit(reference) => {
11786 load_store_device_snapshot_on(conn, reference)?
11787 }
11788 };
11789 let reference = StoreDeviceStateRef::serial(predecessor, &state)
11790 .map_err(|error| DbError::Message(error.to_string()))?;
11791 Ok((reference, state))
11792 }
11793 }
11794 })
11795 .await
11796 }
11797
11798 pub(crate) async fn activated_store_device_registration_records(
11799 &self,
11800 ) -> Result<Vec<(StoreDeviceRegistrationRef, StoreDeviceRegistration)>, DbError> {
11801 let root = self
11802 .local_store_root_ref()
11803 .await?
11804 .ok_or_else(DbError::missing_store_root_hash)?;
11805 self.call(move |conn| {
11806 let mut statement = conn
11807 .prepare(
11808 "SELECT device_id, registration_hash, registration_bytes,
11809 registration_object
11810 FROM store_device_registration_activations ORDER BY device_id",
11811 )
11812 .map_err(DbError::from)?;
11813 let rows = statement
11814 .query_map([], |row| {
11815 Ok((
11816 row.get::<_, String>(0)?,
11817 row.get::<_, String>(1)?,
11818 row.get::<_, Vec<u8>>(2)?,
11819 row.get::<_, String>(3)?,
11820 ))
11821 })
11822 .map_err(DbError::from)?;
11823 rows
11824 .map(|row| {
11825 let (device_id, registration_hash, bytes, object) =
11826 row.map_err(DbError::from)?;
11827 let device_id = device_id.parse().map_err(|error| {
11828 DbError::Message(format!("activated Store device id: {error}"))
11829 })?;
11830 let registration_hash = registration_hash.parse().map_err(|error| {
11831 DbError::Message(format!(
11832 "activated Store device registration hash: {error}"
11833 ))
11834 })?;
11835 let reference: StoreDeviceRegistrationRef =
11836 serde_json::from_str(&object).map_err(|error| {
11837 DbError::Message(format!(
11838 "activated Store device exact reference: {error}"
11839 ))
11840 })?;
11841 if reference.device_id != device_id
11842 || reference.registration_hash != registration_hash
11843 {
11844 return Err(DbError::Message(
11845 "activated Store registration columns differ from its exact reference"
11846 .to_string(),
11847 ));
11848 }
11849 let registration = StoreDeviceRegistration::parse_at(&bytes, &root, device_id)
11850 .map_err(|error| {
11851 DbError::Message(format!(
11852 "activated Store device registration {device_id}: {error}"
11853 ))
11854 })?;
11855 reference.verify_registration(®istration).map_err(|error| {
11856 DbError::Message(format!(
11857 "activated Store device registration {device_id} exact reference: {error}"
11858 ))
11859 })?;
11860 Ok((reference, registration))
11861 })
11862 .collect::<Result<Vec<_>, DbError>>()
11863 })
11864 .await
11865 }
11866
11867 pub(crate) async fn activated_store_device_registration(
11868 &self,
11869 reference: StoreDeviceRegistrationRef,
11870 ) -> Result<StoreDeviceRegistration, DbError> {
11871 let root = self.local_store_root_ref().await?.ok_or_else(|| {
11872 DbError::Message("Store root is absent while loading an activated device".to_string())
11873 })?;
11874 self.call(move |conn| load_activated_registration_on(conn, &root, &reference))
11875 .await
11876 }
11877
11878 pub(crate) async fn local_blob_write_authority(
11879 &self,
11880 ) -> Result<(StoreDeviceRegistrationRef, StoreDeviceRegistration), DbError> {
11881 self.call(|conn| {
11882 local_store_authority_on(conn)
11883 .map(|(_, reference, registration)| (reference, registration))
11884 })
11885 .await
11886 }
11887
11888 pub(crate) async fn activated_store_device_registration_with_authority(
11889 &self,
11890 reference: StoreDeviceRegistrationRef,
11891 ) -> Result<
11892 (
11893 StoreDeviceRegistration,
11894 crate::sync::store_commit::StoreDeviceRegistrationActivation,
11895 ),
11896 DbError,
11897 > {
11898 let root = self.local_store_root_ref().await?.ok_or_else(|| {
11899 DbError::Message("Store root is absent while loading an activated device".to_string())
11900 })?;
11901 self.call(move |conn| {
11902 let registration = load_activated_registration_on(conn, &root, &reference)?;
11903 let authority: String = conn
11904 .query_row(
11905 "SELECT activation_authority FROM store_device_registration_activations \
11906 WHERE device_id = ?1 AND registration_hash = ?2",
11907 (
11908 reference.device_id.to_string(),
11909 reference.registration_hash.to_string(),
11910 ),
11911 |row| row.get(0),
11912 )
11913 .map_err(DbError::from)?;
11914 let authority = serde_json::from_str(&authority).map_err(|error| {
11915 DbError::Message(format!("activated Store registration authority: {error}"))
11916 })?;
11917 Ok((registration, authority))
11918 })
11919 .await
11920 }
11921
11922 pub(crate) async fn activated_store_device_registration_for_device(
11923 &self,
11924 device_id: crate::sync::store_commit::StoreDeviceId,
11925 ) -> Result<
11926 Option<(
11927 StoreDeviceRegistrationRef,
11928 StoreDeviceRegistration,
11929 crate::sync::store_commit::StoreDeviceRegistrationActivation,
11930 )>,
11931 DbError,
11932 > {
11933 let root = self.local_store_root_ref().await?.ok_or_else(|| {
11934 DbError::Message("Store root is absent while loading an activated device".to_string())
11935 })?;
11936 self.call(move |conn| {
11937 let stored: Option<(String, String)> = conn
11938 .query_row(
11939 "SELECT registration_object, activation_authority \
11940 FROM store_device_registration_activations WHERE device_id = ?1",
11941 [device_id.to_string()],
11942 |row| Ok((row.get(0)?, row.get(1)?)),
11943 )
11944 .optional()
11945 .map_err(DbError::from)?;
11946 let Some((reference, authority)) = stored else {
11947 return Ok(None);
11948 };
11949 let reference: StoreDeviceRegistrationRef =
11950 serde_json::from_str(&reference).map_err(|error| {
11951 DbError::Message(format!("activated Store registration ref: {error}"))
11952 })?;
11953 if reference.device_id != device_id {
11954 return Err(DbError::Message(
11955 "activated Store registration row names another device".to_string(),
11956 ));
11957 }
11958 let registration = load_activated_registration_on(conn, &root, &reference)?;
11959 let authority = serde_json::from_str(&authority).map_err(|error| {
11960 DbError::Message(format!("activated Store registration authority: {error}"))
11961 })?;
11962 Ok(Some((reference, registration, authority)))
11963 })
11964 .await
11965 }
11966
11967 pub(crate) fn record_materialized_commit_on(
11968 conn: &Connection,
11969 commit: &StoreBatchCommit,
11970 commit_ref: &StoreBatchCommitRef,
11971 ) -> Result<(), DbError> {
11972 commit_ref
11973 .verify_commit(commit)
11974 .map_err(|error| DbError::Message(error.to_string()))?;
11975 let stored_registration: String = conn
11976 .query_row(
11977 "SELECT registration_object FROM store_device_registration_activations \
11978 WHERE device_id = ?1 AND registration_hash = ?2",
11979 (
11980 commit.author_registration.device_id.to_string(),
11981 commit.author_registration.registration_hash.to_string(),
11982 ),
11983 |row| row.get(0),
11984 )
11985 .map_err(DbError::from)?;
11986 let stored_registration: StoreDeviceRegistrationRef =
11987 serde_json::from_str(&stored_registration).map_err(|error| {
11988 DbError::Message(format!("materialized author registration ref: {error}"))
11989 })?;
11990 if stored_registration != commit.author_registration {
11991 return Err(DbError::Message(
11992 "materialized commit author registration differs from its activation".to_string(),
11993 ));
11994 }
11995 let root = required_store_root_authority_on(conn)?;
11996 if root.store_root_hash != commit.store_root_hash {
11997 return Err(DbError::Message(
11998 "materialized commit belongs to a different Store root".to_string(),
11999 ));
12000 }
12001 let expected_stream =
12002 AuthorStreamId::store_announcements(&root, &commit.author_registration);
12003 let (stream_id, sequence) = match commit_ref.coord {
12004 StoreCommitCoord::MergeConcurrent {
12005 stream_id,
12006 sequence,
12007 } if stream_id == expected_stream => (stream_id.to_string(), sequence),
12008 StoreCommitCoord::MergeConcurrent { .. } => {
12009 return Err(DbError::Message(
12010 "Merge materialization stream differs from its exact author registration"
12011 .to_string(),
12012 ));
12013 }
12014 StoreCommitCoord::Serial { sequence } => (SERIAL_STREAM_ID.to_string(), sequence),
12015 };
12016 if sequence != commit.seq() || commit_ref.coord.policy() != commit.policy() {
12017 return Err(DbError::Message(
12018 "materialization coordinate differs from its signed commit".to_string(),
12019 ));
12020 }
12021 let predecessor = if commit.seq() == 1 {
12022 None
12023 } else if let Some(reference) =
12024 Self::materialized_commit_ref_on(conn, &stream_id, commit.seq() - 1)?
12025 {
12026 Some(reference)
12027 } else {
12028 conn.query_row(
12029 "SELECT commit_ref FROM snapshot_coverage \
12030 WHERE device_id = ?1 AND seq = ?2",
12031 (
12032 &stream_id,
12033 Self::sequence_to_sqlite(&stream_id, commit.seq() - 1)?,
12034 ),
12035 |row| row.get::<_, String>(0),
12036 )
12037 .optional()
12038 .map_err(DbError::from)?
12039 .map(|reference| {
12040 serde_json::from_str(&reference).map_err(|error| {
12041 DbError::Message(format!("snapshot coverage exact commit ref: {error}"))
12042 })
12043 })
12044 .transpose()?
12045 };
12046 if predecessor.as_ref() != commit.order.predecessor() {
12047 return Err(DbError::Message(format!(
12048 "Store commit {}/{} names predecessor {:?}, durable predecessor is {:?}",
12049 stream_id,
12050 commit.seq(),
12051 commit.order.predecessor(),
12052 predecessor
12053 )));
12054 }
12055 let mut device_state = load_declared_store_device_state_on(conn, &commit.device_state)?;
12056 let recovery_author = commit.device_registrations().iter().find_map(|activation| {
12057 if activation.registration != commit.author_registration {
12058 return None;
12059 }
12060 let crate::sync::store_commit::StoreDeviceRegistrationActivationRef::Recovery {
12061 node,
12062 ..
12063 } = &activation.authority
12064 else {
12065 return None;
12066 };
12067 let registration =
12068 load_activated_registration_on(conn, &root, &activation.registration).ok()?;
12069 let crate::sync::store_commit::StoreDeviceRegistrationOrigin::Recovery {
12070 owner_grant,
12071 ..
12072 } = registration.origin
12073 else {
12074 return None;
12075 };
12076 Some((
12077 activation.registration.clone(),
12078 crate::sync::store_commit::OwnerRecoveryCursor {
12079 owner_grant,
12080 position: crate::sync::store_commit::OwnerRecoveryPosition::At {
12081 node: node.clone(),
12082 },
12083 },
12084 ))
12085 });
12086 if let Some((registration, recovery)) = &recovery_author {
12087 device_state = device_state
12088 .activate_registration(registration.clone(), Some(recovery.clone()))
12089 .map_err(|error| DbError::Message(error.to_string()))?;
12090 }
12091 let active_author = device_state
12092 .devices
12093 .get(&commit.author_registration.device_id)
12094 .is_some_and(|record| {
12095 record.registration == commit.author_registration
12096 && matches!(
12097 record.status,
12098 crate::sync::store_commit::StoreDeviceStatus::Active
12099 )
12100 });
12101 if !active_author {
12102 return Err(DbError::Message(
12103 "materialized commit author is not active at its exact predecessor state".into(),
12104 ));
12105 }
12106 for activation in commit.device_registrations() {
12107 if recovery_author
12108 .as_ref()
12109 .is_some_and(|(registration, _)| registration == &activation.registration)
12110 {
12111 continue;
12112 }
12113 device_state = device_state
12114 .activate_registration(activation.registration.clone(), None)
12115 .map_err(|error| DbError::Message(error.to_string()))?;
12116 }
12117 for retirement in commit.device_retirements() {
12118 device_state = device_state
12119 .self_retire(retirement.clone())
12120 .map_err(|error| DbError::Message(error.to_string()))?;
12121 }
12122 let seq = Self::sequence_to_sqlite(&stream_id, commit.seq())?;
12123 let commit_ref_json = serde_json::to_string(commit_ref).map_err(|error| {
12124 DbError::Message(format!("serialize exact Store commit ref: {error}"))
12125 })?;
12126 conn.execute(
12127 "INSERT INTO store_device_state_snapshots (commit_ref, state) VALUES (?1, ?2)",
12128 rusqlite::params![
12129 &commit_ref_json,
12130 serde_json::to_string(&device_state).map_err(|error| {
12131 DbError::Message(format!(
12132 "serialize materialized Store device state: {error}"
12133 ))
12134 })?,
12135 ],
12136 )
12137 .map_err(DbError::from)?;
12138 conn.execute(
12139 "INSERT INTO materialized_commits (device_id, seq, commit_ref) \
12140 VALUES (?1, ?2, ?3)",
12141 (&stream_id, seq, commit_ref_json),
12142 )
12143 .map(|_| ())
12144 .map_err(DbError::from)
12145 }
12146
12147 pub(crate) fn record_verified_circle_activations_on(
12148 conn: &Connection,
12149 commit: &StoreBatchCommit,
12150 commit_ref: &StoreBatchCommitRef,
12151 activations: &[crate::sync::circle_ops::VerifiedCircleReference],
12152 ) -> Result<(), DbError> {
12153 if activations.len() != commit.circle_controls().len() {
12154 return Err(DbError::Message(
12155 "verified circle activations do not cover every control reference".to_string(),
12156 ));
12157 }
12158 commit_ref
12159 .verify_commit(commit)
12160 .map_err(|error| DbError::Message(error.to_string()))?;
12161 let stream_id = match &commit_ref.coord {
12162 StoreCommitCoord::MergeConcurrent { stream_id, .. } => stream_id.to_string(),
12163 StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
12164 };
12165 let seq = Self::sequence_to_sqlite(&stream_id, commit_ref.coord.sequence())?;
12166 for activation in activations {
12167 if !commit.circle_controls().contains(&activation.reference)
12168 || activation.reference.circle_id() != activation.circle_id
12169 || activation.reference.control() != &activation.control.coord
12170 || !activation.control.verify()
12171 {
12172 return Err(DbError::Message(
12173 "verified circle activation differs from Store control reference".to_string(),
12174 ));
12175 }
12176 let circle_id = activation.circle_id.to_string();
12177 if let Some(access) = &activation.local_access {
12178 let leaf = &access.leaf.value;
12179 if activation
12180 .control
12181 .value
12182 .owners
12183 .binary_search(&leaf.owner_pubkey)
12184 .is_err()
12185 {
12186 return Err(DbError::Message(format!(
12187 "circle {circle_id} local access signer is not a control Owner"
12188 )));
12189 }
12190 match (&leaf.disposition, &access.active) {
12191 (crate::sync::circle::CircleAccessDisposition::Active { .. }, Some(_))
12192 | (crate::sync::circle::CircleAccessDisposition::Inactive, None) => {}
12193 _ => {
12194 return Err(DbError::Message(format!(
12195 "circle {circle_id} access state differs from its disposition"
12196 )));
12197 }
12198 }
12199 }
12200 let mut statement = conn
12201 .prepare(
12202 "SELECT control_bytes FROM circle_control_activations
12203 WHERE circle_id = ?1",
12204 )
12205 .map_err(DbError::from)?;
12206 let rows = statement
12207 .query_map([&circle_id], |row| row.get::<_, Vec<u8>>(0))
12208 .map_err(DbError::from)?;
12209 let mut existing_controls = Vec::new();
12210 for bytes in rows {
12211 let bytes = bytes.map_err(DbError::from)?;
12212 let control: crate::sync::circle::CircleControl = serde_json::from_slice(&bytes)
12213 .map_err(|error| {
12214 DbError::Message(format!("parse activated circle control: {error}"))
12215 })?;
12216 existing_controls.push(control);
12217 }
12218 drop(statement);
12219 match activation.control.value.order.previous_control_hash() {
12220 None if !existing_controls.is_empty() => {
12221 return Err(DbError::Message(format!(
12222 "circle {circle_id} already has a founder control"
12223 )));
12224 }
12225 None => {}
12226 Some(previous_hash) => {
12227 let previous = existing_controls
12228 .iter()
12229 .find(|control| control.control_hash() == previous_hash)
12230 .ok_or_else(|| {
12231 DbError::Message(format!(
12232 "circle {circle_id} control predecessor {previous_hash} is absent"
12233 ))
12234 })?;
12235 if previous.store_root_hash != activation.control.value.store_root_hash
12236 || previous.circle_id != activation.circle_id
12237 || previous
12238 .owners
12239 .binary_search(&activation.control.value.author_pubkey)
12240 .is_err()
12241 || activation.control.value.order.ordinal()
12242 != previous.order.ordinal().checked_add(1).ok_or_else(|| {
12243 DbError::Message("circle control ordinal overflow".to_string())
12244 })?
12245 {
12246 return Err(DbError::Message(format!(
12247 "circle {circle_id} control does not extend its authorized predecessor"
12248 )));
12249 }
12250 }
12251 }
12252 let control_coord =
12253 serde_json::to_string(&activation.control.coord).map_err(|error| {
12254 DbError::Message(format!("serialize circle control coordinate: {error}"))
12255 })?;
12256 conn.execute(
12257 "INSERT INTO circle_control_activations
12258 (circle_id, control_coord, stream_id, seq, commit_hash, control_bytes)
12259 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
12260 rusqlite::params![
12261 &circle_id,
12262 &control_coord,
12263 stream_id,
12264 seq,
12265 commit.commit_hash().to_string(),
12266 &activation.control.bytes,
12267 ],
12268 )
12269 .map_err(DbError::from)?;
12270 let Some(access) = &activation.local_access else {
12271 continue;
12272 };
12273 let disposition = match access.leaf.value.disposition {
12274 crate::sync::circle::CircleAccessDisposition::Active { .. } => "active",
12275 crate::sync::circle::CircleAccessDisposition::Inactive => "inactive",
12276 };
12277 conn.execute(
12278 "INSERT INTO circle_access_cache
12279 (circle_id, control_coord, owner_pubkey, disposition, access_bytes)
12280 VALUES (?1, ?2, ?3, ?4, ?5)",
12281 rusqlite::params![
12282 &circle_id,
12283 &control_coord,
12284 &access.leaf.value.owner_pubkey,
12285 disposition,
12286 &access.leaf.bytes,
12287 ],
12288 )
12289 .map_err(DbError::from)?;
12290 if let Some(active) = &access.active {
12291 let roster_bytes = serde_json::to_vec(&active.roster).map_err(|error| {
12292 DbError::Message(format!("serialize activated circle roster: {error}"))
12293 })?;
12294 conn.execute(
12295 "INSERT INTO circle_roster_cache (circle_id, control_coord, roster_bytes)
12296 VALUES (?1, ?2, ?3)",
12297 rusqlite::params![&circle_id, &control_coord, roster_bytes,],
12298 )
12299 .map_err(DbError::from)?;
12300 let metadata_bytes = serde_json::to_vec(&active.metadata).map_err(|error| {
12301 DbError::Message(format!("serialize activated circle metadata: {error}"))
12302 })?;
12303 conn.execute(
12304 "INSERT INTO circle_metadata_cache
12305 (circle_id, control_coord, metadata_bytes) VALUES (?1, ?2, ?3)",
12306 rusqlite::params![&circle_id, &control_coord, metadata_bytes,],
12307 )
12308 .map_err(DbError::from)?;
12309 }
12310 }
12311 Ok(())
12312 }
12313
12314 pub(crate) fn record_materialized_serial_commit_on(
12315 conn: &Connection,
12316 commit: &StoreBatchCommit,
12317 commit_ref: &StoreBatchCommitRef,
12318 authorization: &SerialAuthorizationState,
12319 ) -> Result<(), DbError> {
12320 if commit.policy() != WritePolicy::Serial {
12321 return Err(DbError::Message(
12322 "Serial membership state cannot accompany a MergeConcurrent commit".to_string(),
12323 ));
12324 }
12325 Self::record_materialized_commit_on(conn, commit, commit_ref)?;
12326 let membership = serde_json::to_string(&authorization.membership).map_err(|error| {
12327 DbError::Message(format!("serialize Serial membership state: {error}"))
12328 })?;
12329 let provider_admin =
12330 serde_json::to_string(&authorization.provider_admin).map_err(|error| {
12331 DbError::Message(format!(
12332 "serialize Serial provider administrator state: {error}"
12333 ))
12334 })?;
12335 conn.execute(
12336 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12337 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12338 (SERIAL_MEMBERSHIP_STATE_KEY, membership),
12339 )
12340 .map_err(DbError::from)?;
12341 conn.execute(
12342 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12343 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12344 (SERIAL_PROVIDER_ADMIN_STATE_KEY, provider_admin),
12345 )
12346 .map_err(DbError::from)?;
12347 conn.execute(
12348 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12349 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12350 (
12351 SERIAL_KEY_GENERATION_STATE_KEY,
12352 authorization.key_generation.to_string(),
12353 ),
12354 )
12355 .map(|_| ())
12356 .map_err(DbError::from)
12357 }
12358
12359 pub async fn serial_membership_state(&self) -> Result<Option<SerialMembershipState>, DbError> {
12360 let Some(raw) = self.get_protocol_state(SERIAL_MEMBERSHIP_STATE_KEY).await? else {
12361 return Ok(None);
12362 };
12363 serde_json::from_str(&raw)
12364 .map(Some)
12365 .map_err(|error| DbError::Message(format!("parse Serial membership state: {error}")))
12366 }
12367
12368 pub async fn serial_authorization_state(
12369 &self,
12370 ) -> Result<Option<SerialAuthorizationState>, DbError> {
12371 let membership = self.get_protocol_state(SERIAL_MEMBERSHIP_STATE_KEY).await?;
12372 let provider_admin = self
12373 .get_protocol_state(SERIAL_PROVIDER_ADMIN_STATE_KEY)
12374 .await?;
12375 let key_generation = self
12376 .get_protocol_state(SERIAL_KEY_GENERATION_STATE_KEY)
12377 .await?;
12378 match (membership, provider_admin, key_generation) {
12379 (None, None, None) => Ok(None),
12380 (Some(membership), Some(provider_admin), Some(key_generation)) => {
12381 let membership = serde_json::from_str(&membership).map_err(|error| {
12382 DbError::Message(format!("parse Serial membership state: {error}"))
12383 })?;
12384 let provider_admin: ProviderAdminState = serde_json::from_str(&provider_admin)
12385 .map_err(|error| {
12386 DbError::Message(format!(
12387 "parse Serial provider administrator state: {error}"
12388 ))
12389 })?;
12390 let key_generation = key_generation.parse::<u64>().map_err(|error| {
12391 DbError::Message(format!("parse Serial key generation: {error}"))
12392 })?;
12393 Ok(Some(SerialAuthorizationState {
12394 membership,
12395 provider_admin,
12396 key_generation,
12397 }))
12398 }
12399 _ => Err(DbError::Message(
12400 "Serial authorization state is only partially durable".to_string(),
12401 )),
12402 }
12403 }
12404
12405 pub async fn serial_key_generation(&self) -> Result<Option<u64>, DbError> {
12406 let Some(raw) = self
12407 .get_protocol_state(SERIAL_KEY_GENERATION_STATE_KEY)
12408 .await?
12409 else {
12410 return Ok(None);
12411 };
12412 raw.parse::<u64>()
12413 .map(Some)
12414 .map_err(|error| DbError::Message(format!("parse Serial key generation: {error}")))
12415 }
12416
12417 fn install_serial_root_authorization_on(
12418 conn: &Connection,
12419 founder_pubkey: &str,
12420 authorization: &SerialAuthorizationState,
12421 ) -> Result<(), DbError> {
12422 if Self::latest_position_for_device_on(conn, SERIAL_STREAM_ID)?.is_some() {
12423 return Err(DbError::Message(
12424 "cannot install founder-only Serial authorization after a materialized commit"
12425 .to_string(),
12426 ));
12427 }
12428 let existing_state: i64 = conn
12429 .query_row(
12430 "SELECT COUNT(*) FROM protocol_state WHERE key IN (?1, ?2, ?3, ?4)",
12431 rusqlite::params![
12432 crate::sync::membership_ops::OWNER_PUBKEY_STATE_KEY,
12433 SERIAL_MEMBERSHIP_STATE_KEY,
12434 SERIAL_KEY_GENERATION_STATE_KEY,
12435 SERIAL_PROVIDER_ADMIN_STATE_KEY,
12436 ],
12437 |row| row.get(0),
12438 )
12439 .map_err(DbError::from)?;
12440 if existing_state != 0 {
12441 return Err(DbError::Message(
12442 "cannot install founder-only Serial authorization over existing state".to_string(),
12443 ));
12444 }
12445 let membership = serde_json::to_string(&authorization.membership).map_err(|error| {
12446 DbError::Message(format!(
12447 "serialize Serial founder membership state: {error}"
12448 ))
12449 })?;
12450 let provider_admin =
12451 serde_json::to_string(&authorization.provider_admin).map_err(|error| {
12452 DbError::Message(format!(
12453 "serialize Serial founder provider administrator state: {error}"
12454 ))
12455 })?;
12456 for (key, value) in [
12457 (
12458 crate::sync::membership_ops::OWNER_PUBKEY_STATE_KEY,
12459 founder_pubkey.to_string(),
12460 ),
12461 (SERIAL_MEMBERSHIP_STATE_KEY, membership),
12462 (SERIAL_PROVIDER_ADMIN_STATE_KEY, provider_admin),
12463 (
12464 SERIAL_KEY_GENERATION_STATE_KEY,
12465 authorization.key_generation.to_string(),
12466 ),
12467 ] {
12468 conn.execute(
12469 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12470 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12471 (key, value),
12472 )
12473 .map_err(DbError::from)?;
12474 }
12475 Ok(())
12476 }
12477
12478 pub(crate) async fn install_serial_root_authorization(
12479 &self,
12480 founder_pubkey: String,
12481 authorization: SerialAuthorizationState,
12482 ) -> Result<(), DbError> {
12483 self.call(move |conn| {
12484 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
12485 Self::install_serial_root_authorization_on(&tx, &founder_pubkey, &authorization)?;
12486 tx.commit().map_err(DbError::from)
12487 })
12488 .await
12489 }
12490
12491 pub(crate) async fn materialize_serial_control_commit(
12492 &self,
12493 commit: StoreBatchCommit,
12494 commit_ref: StoreBatchCommitRef,
12495 authorization_after: SerialAuthorizationState,
12496 ) -> Result<(), DbError> {
12497 if commit.control().is_none() || commit.store_package().is_some() {
12498 return Err(DbError::Message(
12499 "Serial control materialization requires a control-only Store batch".to_string(),
12500 ));
12501 }
12502 self.call(move |conn| {
12503 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
12504 Self::record_materialized_serial_commit_on(
12505 &tx,
12506 &commit,
12507 &commit_ref,
12508 &authorization_after,
12509 )?;
12510 tx.commit().map_err(DbError::from)
12511 })
12512 .await
12513 }
12514
12515 pub(crate) async fn install_serial_authorization_at_position(
12516 &self,
12517 expected: StoreBatchCommitRef,
12518 authorization: SerialAuthorizationState,
12519 ) -> Result<(), DbError> {
12520 self.call(move |conn| {
12521 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
12522 let actual = Self::latest_position_for_device_on(&tx, SERIAL_STREAM_ID)?;
12523 if actual.as_ref() != Some(&expected) {
12524 return Err(DbError::Message(format!(
12525 "cannot install Serial authorization at {expected:?}; durable position is {actual:?}"
12526 )));
12527 }
12528 let membership = serde_json::to_string(&authorization.membership).map_err(|error| {
12529 DbError::Message(format!("serialize Serial membership state: {error}"))
12530 })?;
12531 let provider_admin = serde_json::to_string(&authorization.provider_admin).map_err(
12532 |error| {
12533 DbError::Message(format!(
12534 "serialize Serial provider administrator state: {error}"
12535 ))
12536 },
12537 )?;
12538 tx.execute(
12539 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12540 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12541 (SERIAL_MEMBERSHIP_STATE_KEY, membership),
12542 )
12543 .map_err(DbError::from)?;
12544 tx.execute(
12545 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12546 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12547 (SERIAL_PROVIDER_ADMIN_STATE_KEY, provider_admin),
12548 )
12549 .map_err(DbError::from)?;
12550 tx.execute(
12551 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2) \
12552 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
12553 (
12554 SERIAL_KEY_GENERATION_STATE_KEY,
12555 authorization.key_generation.to_string(),
12556 ),
12557 )
12558 .map_err(DbError::from)?;
12559 tx.commit().map_err(DbError::from)
12560 })
12561 .await
12562 }
12563
12564 pub(crate) async fn install_bootstrap_state(
12565 &self,
12566 coverage: &CommitFrontier,
12567 snapshot: PublishedStoreSnapshot,
12568 store_root: crate::sync::store_objects::VerifiedObject<StoreProtocolRoot>,
12569 founder: crate::sync::store_objects::VerifiedObject<StoreDeviceRegistration>,
12570 ) -> Result<(), DbError> {
12571 if store_root.value.to_bytes() != store_root.bytes
12572 || store_root.value.object_hash() != store_root.semantic_hash
12573 {
12574 return Err(DbError::Message(
12575 "bootstrap Store root differs from its verified object".to_string(),
12576 ));
12577 }
12578 let store_root_ref = crate::sync::store_commit::StoreRootRef {
12579 store_root_id: store_root.value.descriptor.store_root_id(),
12580 store_root_hash: store_root.semantic_hash,
12581 object: store_root.object,
12582 };
12583 let founder_reference =
12584 StoreDeviceRegistrationRef::from_registration(&founder.value, founder.object.clone());
12585 if founder.semantic_hash != founder_reference.registration_hash {
12586 return Err(DbError::Message(
12587 "bootstrap founder semantic hash differs from its exact registration".to_string(),
12588 ));
12589 }
12590 let genesis = ResolvedStoreDeviceState::founder(
12591 &store_root_ref,
12592 founder_reference.clone(),
12593 &store_root.value.descriptor.founder_pubkey,
12594 store_root.value.descriptor.founder_grant.clone(),
12595 &store_root.value.descriptor.founder_recovery,
12596 )
12597 .map_err(|error| DbError::Message(error.to_string()))?;
12598 if coverage.policy() != self.write_policy() {
12599 return Err(DbError::Message(format!(
12600 "snapshot coverage uses {:?}, database uses {:?}",
12601 coverage.policy(),
12602 self.write_policy()
12603 )));
12604 }
12605 if snapshot.meta.coverage != *coverage
12606 || snapshot.meta.store_root_hash != store_root_ref.store_root_hash
12607 || snapshot.meta.snapshot_hash() != snapshot.reference.snapshot_hash
12608 || snapshot.meta.successor.next_slot != snapshot.successor_slot
12609 {
12610 return Err(DbError::Message(
12611 "bootstrap snapshot state differs from its exact signed metadata".to_string(),
12612 ));
12613 }
12614 let coverage = coverage.clone().into_refs();
12615 self.call(move |conn| {
12616 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
12617 validate_snapshot_object_owners_on(&tx, &store_root_ref, &snapshot.meta)?;
12618 install_store_root_authority_on(&tx, &store_root_ref, &store_root.bytes)?;
12619 install_store_founder_state_on(
12620 &tx,
12621 &store_root_ref,
12622 &founder_reference,
12623 &founder.value,
12624 &founder.bytes,
12625 &genesis,
12626 )?;
12627 tx.execute("DELETE FROM snapshot_coverage", [])
12628 .map_err(DbError::from)?;
12629 for (stream_id, reference) in coverage {
12630 let encoded = serde_json::to_string(&reference).map_err(|error| {
12631 DbError::Message(format!("serialize snapshot exact commit ref: {error}"))
12632 })?;
12633 tx.execute(
12634 "INSERT INTO snapshot_coverage \
12635 (device_id, seq, commit_ref, snapshot_hash) VALUES (?1, ?2, ?3, ?4)",
12636 (
12637 &stream_id,
12638 Self::sequence_to_sqlite(&stream_id, reference.coord.sequence())?,
12639 encoded,
12640 snapshot.reference.snapshot_hash.to_string(),
12641 ),
12642 )
12643 .map_err(DbError::from)?;
12644 }
12645 tx.commit().map_err(DbError::from)
12646 })
12647 .await
12648 }
12649
12650 pub(crate) async fn install_device_join_bootstrap(
12651 &self,
12652 root: crate::sync::store_commit::StoreRootRef,
12653 plan: crate::sync::store_pull::DeviceJoinBootstrapPlan,
12654 ) -> Result<(), DbError> {
12655 if plan.coverage.policy() != self.write_policy() {
12656 return Err(DbError::Message(
12657 "device join bootstrap cut differs from the database write policy".to_string(),
12658 ));
12659 }
12660 self.call(move |conn| {
12661 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
12662 let installed_root = required_store_root_authority_on(&tx)?;
12663 if installed_root != root || plan.founder.store_root != root {
12664 return Err(DbError::Message(
12665 "device join bootstrap root differs from the installed exact root".to_string(),
12666 ));
12667 }
12668 install_store_founder_state_on(
12669 &tx,
12670 &root,
12671 &plan.founder_reference,
12672 &plan.founder,
12673 &plan.founder_bytes,
12674 &plan.genesis,
12675 )?;
12676
12677 let coverage_refs = match plan.coverage.clone() {
12678 crate::sync::store_commit::StoreHistoryCut::MergeConcurrent(frontier) => frontier
12679 .into_iter()
12680 .map(|(stream_id, reference)| (stream_id.to_string(), reference))
12681 .collect::<BTreeMap<_, _>>(),
12682 crate::sync::store_commit::StoreHistoryCut::Serial(
12683 StoreSerialPredecessor::Commit(reference),
12684 ) => BTreeMap::from([(SERIAL_STREAM_ID.to_string(), reference)]),
12685 crate::sync::store_commit::StoreHistoryCut::Serial(
12686 StoreSerialPredecessor::Genesis { .. },
12687 ) => BTreeMap::new(),
12688 };
12689 let mut coverage = BTreeMap::new();
12690 for (stream_id, reference) in coverage_refs {
12691 let sequence = reference.coord.sequence();
12692 let materialized = Self::materialized_commit_ref_on(&tx, &stream_id, sequence)?;
12693 if materialized.as_ref().is_some_and(|stored| stored != &reference) {
12694 return Err(DbError::Message(format!(
12695 "device join bootstrap conflicts with materialized commit at {stream_id}/{sequence}"
12696 )));
12697 }
12698 let snapshot = tx
12699 .query_row(
12700 "SELECT commit_ref FROM snapshot_coverage
12701 WHERE device_id = ?1 AND seq = ?2",
12702 (
12703 &stream_id,
12704 Self::sequence_to_sqlite(&stream_id, sequence)?,
12705 ),
12706 |row| row.get::<_, String>(0),
12707 )
12708 .optional()
12709 .map_err(DbError::from)?
12710 .map(|encoded| Self::parse_stored_commit_ref(&stream_id, sequence, &encoded))
12711 .transpose()?;
12712 if snapshot.as_ref().is_some_and(|stored| stored != &reference) {
12713 return Err(DbError::Message(format!(
12714 "device join bootstrap conflicts with snapshot coverage at {stream_id}/{sequence}"
12715 )));
12716 }
12717 coverage.insert(
12718 stream_id,
12719 (reference, materialized.is_some() || snapshot.is_some()),
12720 );
12721 }
12722
12723 for prepared in &plan.commits {
12724 let stream_id = match &prepared.reference.coord {
12725 StoreCommitCoord::MergeConcurrent { stream_id, .. } => stream_id.to_string(),
12726 StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
12727 };
12728 let already_represented = coverage.get(&stream_id).is_some_and(
12729 |(reference, represented)| {
12730 *represented
12731 && prepared.reference.coord.sequence()
12732 <= reference.coord.sequence()
12733 },
12734 );
12735 if !already_represented
12736 && (prepared.commit.store_package().is_some()
12737 || !prepared.commit.circle_packages().is_empty())
12738 {
12739 return Err(DbError::Message(format!(
12740 "device join bootstrap cannot advance over unmaterialized row data at {stream_id}/{}",
12741 prepared.reference.coord.sequence()
12742 )));
12743 }
12744 }
12745
12746 for prepared in plan.commits {
12747 let stream_id = match &prepared.reference.coord {
12748 StoreCommitCoord::MergeConcurrent { stream_id, .. } => stream_id.to_string(),
12749 StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
12750 };
12751 if coverage.get(&stream_id).is_some_and(|(reference, represented)| {
12752 *represented
12753 && prepared.reference.coord.sequence() <= reference.coord.sequence()
12754 }) {
12755 continue;
12756 }
12757 if let Some(existing) = Self::materialized_commit_ref_on(
12758 &tx,
12759 &stream_id,
12760 prepared.reference.coord.sequence(),
12761 )? {
12762 if existing != prepared.reference {
12763 return Err(DbError::Message(format!(
12764 "device join bootstrap conflicts at {stream_id}/{}",
12765 prepared.reference.coord.sequence()
12766 )));
12767 }
12768 continue;
12769 }
12770 Self::record_activated_store_device_registrations_on(
12771 &tx,
12772 &prepared.commit,
12773 &prepared.registrations,
12774 )?;
12775 Self::record_materialized_commit_on(&tx, &prepared.commit, &prepared.reference)?;
12776 }
12777 tx.commit().map_err(DbError::from)
12778 })
12779 .await
12780 }
12781
12782 fn sequence_from_sqlite(device_id: &str, value: i64) -> Result<u64, DbError> {
12783 let value = u64::try_from(value).map_err(|_| {
12784 DbError::Message(format!(
12785 "Store position for {device_id:?} contains negative sequence {value}"
12786 ))
12787 })?;
12788 if value == 0 {
12789 return Err(DbError::Message(format!(
12790 "Store position for {device_id:?} contains sequence zero"
12791 )));
12792 }
12793 Ok(value)
12794 }
12795
12796 fn sequence_to_sqlite(device_id: &str, value: u64) -> Result<i64, DbError> {
12797 if value == 0 {
12798 return Err(DbError::Message(format!(
12799 "Store position for {device_id:?} cannot use sequence zero"
12800 )));
12801 }
12802 i64::try_from(value).map_err(|_| {
12803 DbError::Message(format!(
12804 "Store position for {device_id:?} exceeds SQLite INTEGER"
12805 ))
12806 })
12807 }
12808
12809 pub(crate) fn persist_prepared_audience_objects_on(
12810 conn: &Connection,
12811 write_id: &WriteId,
12812 packages: &[PreparedAudiencePackage],
12813 blobs: &[PreparedAudienceBlob],
12814 ) -> Result<(), DbError> {
12815 let package_audiences = packages
12816 .iter()
12817 .map(|prepared| {
12818 if prepared.package().write_id() != write_id {
12819 return Err(DbError::Message(format!(
12820 "prepared audience package write {} differs from journal write {write_id}",
12821 prepared.package().write_id()
12822 )));
12823 }
12824 Ok(prepared.package().audience().remote_audience())
12825 })
12826 .collect::<Result<std::collections::BTreeSet<_>, DbError>>()?;
12827 if package_audiences.len() != packages.len() {
12828 return Err(DbError::Message(format!(
12829 "write {write_id} has duplicate prepared package audiences"
12830 )));
12831 }
12832 for prepared in packages {
12833 let audience = prepared.package().audience().remote_audience();
12834 validate_remote_object_on(
12835 conn,
12836 prepared.remote_object_id(),
12837 prepared.object(),
12838 prepared.semantic_bytes(),
12839 RemoteStoredRepresentationRef::Inline(prepared.stored_bytes()),
12840 )?;
12841 conn.execute(
12842 "INSERT INTO store_write_packages
12843 (write_id, audience, remote_object_id)
12844 VALUES (?1, ?2, ?3)
12845 ON CONFLICT(write_id, audience) DO NOTHING",
12846 rusqlite::params![
12847 write_id.as_str(),
12848 remote_audience_to_db(&audience),
12849 prepared.remote_object_id().to_string(),
12850 ],
12851 )
12852 .map_err(DbError::from)?;
12853 validate_prepared_package_on(conn, write_id, prepared)?;
12854 }
12855 for prepared in blobs {
12856 if !package_audiences.contains(prepared.audience()) {
12857 return Err(DbError::Message(format!(
12858 "write {write_id} has a prepared blob for {:?} without that audience's package",
12859 prepared.audience()
12860 )));
12861 }
12862 let locator = prepared.blob().locator();
12863 validate_remote_object_on(
12864 conn,
12865 prepared.remote_object_id(),
12866 prepared.blob().object(),
12867 &locator.to_bytes(),
12868 RemoteStoredRepresentationRef::Blob,
12869 )?;
12870 let locator_hash = locator.locator_hash();
12871 let spool_path = prepared
12872 .spool_path()
12873 .map(|path| {
12874 path.to_str().map(str::to_string).ok_or_else(|| {
12875 DbError::Message("prepared blob spool path is not UTF-8".to_string())
12876 })
12877 })
12878 .transpose()?;
12879 conn.execute(
12880 "INSERT INTO store_write_blobs
12881 (write_id, audience, locator_hash, remote_object_id, spool_path)
12882 VALUES (?1, ?2, ?3, ?4, ?5)
12883 ON CONFLICT(write_id, audience, remote_object_id) DO NOTHING",
12884 rusqlite::params![
12885 write_id.as_str(),
12886 remote_audience_to_db(prepared.audience()),
12887 locator_hash.to_string(),
12888 prepared.remote_object_id().to_string(),
12889 spool_path,
12890 ],
12891 )
12892 .map_err(DbError::from)?;
12893 validate_prepared_blob_on(conn, write_id, prepared)?;
12894 }
12895 Ok(())
12896 }
12897
12898 fn persist_closed_write_objects_on(
12899 conn: &Connection,
12900 write_id: &WriteId,
12901 store_root_hash: ObjectHash,
12902 commit_ref: &StoreBatchCommitRef,
12903 commit: &StoreBatchCommit,
12904 commit_stored_bytes: &[u8],
12905 partitions: &PreparedStoreWritePartitions,
12906 remote_objects: &[RemoteObjectRecord],
12907 audiences: &PreparedAudienceObjects,
12908 ) -> Result<(), DbError> {
12909 let mut object_ids = std::collections::BTreeSet::new();
12910 for remote in remote_objects {
12911 remote
12912 .validate()
12913 .map_err(|error| DbError::Message(format!("prepared remote object: {error}")))?;
12914 if !object_ids.insert(remote.object_id()) {
12915 return Err(DbError::Message(
12916 "prepared write contains a duplicate remote object".to_string(),
12917 ));
12918 }
12919 }
12920 validate_prepared_audience_blob_graph(&object_ids, audiences)?;
12921 for remote in remote_objects {
12922 let object_id = remote.object_id();
12923 let existing = conn
12924 .query_row(
12925 "SELECT state FROM remote_objects WHERE object_id = ?1",
12926 [object_id.to_string()],
12927 |row| row.get::<_, String>(0),
12928 )
12929 .optional()
12930 .map_err(DbError::from)?;
12931 let persisted = match existing {
12932 Some(state) => {
12933 let existing: RemoteObjectRecord = serde_json::from_str(&state).map_err(
12934 |error| {
12935 DbError::Message(format!(
12936 "prepared remote object {object_id} has invalid closed state: {error}"
12937 ))
12938 },
12939 )?;
12940 merge_prepared_remote_object(existing, remote, commit_ref)?
12941 }
12942 None => remote.clone(),
12943 };
12944 let state = serde_json::to_string(&persisted).map_err(|error| {
12945 DbError::Message(format!("serialize closed remote object: {error}"))
12946 })?;
12947 conn.execute(
12948 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)
12949 ON CONFLICT(object_id) DO UPDATE SET state = excluded.state",
12950 (object_id.to_string(), state),
12951 )
12952 .map_err(DbError::from)?;
12953 }
12954 let commit_remote = RemoteObjectRecord::candidate_commit(
12955 commit_ref.clone(),
12956 commit.to_bytes(),
12957 commit_stored_bytes.to_vec(),
12958 )
12959 .map_err(|error| DbError::Message(format!("prepared candidate commit: {error}")))?;
12960 persist_exact_remote_object_on(conn, &commit_remote, "candidate commit")?;
12961 let expected_partition_count = usize::from(partitions.store.is_some())
12962 .checked_add(partitions.circles.len())
12963 .ok_or_else(|| DbError::Message("audience partition count overflow".to_string()))?;
12964 if audiences.packages.len() != expected_partition_count {
12965 return Err(DbError::Message(
12966 "prepared audience packages do not cover every write partition".to_string(),
12967 ));
12968 }
12969 let mut indexed = std::collections::BTreeSet::new();
12970 for package in &audiences.packages {
12971 let value = package.package();
12972 if value.store_root_hash() != store_root_hash
12973 || value.write_id() != write_id
12974 || value.commit_coord() != &commit_ref.coord
12975 || value.candidate_family() != commit.candidate_family()
12976 {
12977 return Err(DbError::Message(
12978 "prepared audience package differs from its exact Store commit".to_string(),
12979 ));
12980 }
12981 match value.audience() {
12982 crate::sync::audience_package::PackageAudience::Store => {
12983 let partition = partitions.store.as_ref().ok_or_else(|| {
12984 DbError::Message(
12985 "prepared Store package has no Store partition".to_string(),
12986 )
12987 })?;
12988 if value.changeset() != partition.changeset {
12989 return Err(DbError::Message(
12990 "prepared Store package changeset differs from its partition"
12991 .to_string(),
12992 ));
12993 }
12994 commit
12995 .verify_store_package(package.semantic_bytes())
12996 .map_err(|error| DbError::Message(error.to_string()))?;
12997 }
12998 crate::sync::audience_package::PackageAudience::Circle { circle_id, .. } => {
12999 let partition = partitions
13000 .circles
13001 .iter()
13002 .find(|partition| partition.audience == Audience::Circle(*circle_id))
13003 .ok_or_else(|| {
13004 DbError::Message(format!(
13005 "prepared Circle package {circle_id} has no partition"
13006 ))
13007 })?;
13008 if value.changeset() != partition.changeset {
13009 return Err(DbError::Message(format!(
13010 "prepared Circle package {circle_id} changeset differs from its partition"
13011 )));
13012 }
13013 commit
13014 .verify_circle_package(*circle_id, package.semantic_bytes())
13015 .map_err(|error| DbError::Message(error.to_string()))?;
13016 }
13017 }
13018 indexed.insert(package.remote_object_id());
13019 }
13020 indexed.extend(
13021 audiences
13022 .blobs
13023 .iter()
13024 .map(PreparedAudienceBlob::remote_object_id),
13025 );
13026 debug_assert_eq!(indexed, object_ids);
13027 Self::persist_prepared_audience_objects_on(
13028 conn,
13029 write_id,
13030 &audiences.packages,
13031 &audiences.blobs,
13032 )
13033 }
13034
13035 fn validate_loaded_write_objects(
13036 write_id: &WriteId,
13037 commit_ref: &StoreBatchCommitRef,
13038 commit: &StoreBatchCommit,
13039 partitions: &PreparedStoreWritePartitions,
13040 audiences: &PreparedAudienceObjects,
13041 ) -> Result<(), DbError> {
13042 let expected_package_count = usize::from(commit.store_package().is_some())
13043 .checked_add(commit.circle_packages().len())
13044 .ok_or_else(|| DbError::Message("package count overflow".to_string()))?;
13045 let partition_count = usize::from(partitions.store.is_some())
13046 .checked_add(partitions.circles.len())
13047 .ok_or_else(|| DbError::Message("audience partition count overflow".to_string()))?;
13048 if audiences.packages.len() != expected_package_count
13049 || audiences.packages.len() != partition_count
13050 {
13051 return Err(DbError::Message(
13052 "prepared package indexes do not exactly cover commit audiences".to_string(),
13053 ));
13054 }
13055 for package in &audiences.packages {
13056 let value = package.package();
13057 if value.write_id() != write_id
13058 || value.commit_coord() != &commit_ref.coord
13059 || value.candidate_family() != commit.candidate_family()
13060 {
13061 return Err(DbError::Message(
13062 "indexed audience package differs from its exact commit".to_string(),
13063 ));
13064 }
13065 let expected_object = match value.audience() {
13066 crate::sync::audience_package::PackageAudience::Store => {
13067 commit
13068 .verify_store_package(package.semantic_bytes())
13069 .map_err(|error| DbError::Message(error.to_string()))?;
13070 &commit
13071 .store_package()
13072 .as_ref()
13073 .expect("verified present")
13074 .object
13075 }
13076 crate::sync::audience_package::PackageAudience::Circle { circle_id, .. } => {
13077 commit
13078 .verify_circle_package(*circle_id, package.semantic_bytes())
13079 .map_err(|error| DbError::Message(error.to_string()))?;
13080 &commit
13081 .circle_packages()
13082 .iter()
13083 .find(|entry| entry.circle_id == *circle_id)
13084 .expect("verified present")
13085 .package
13086 .object
13087 }
13088 };
13089 if package.object() != expected_object {
13090 return Err(DbError::Message(
13091 "indexed audience package exact object differs from its commit".to_string(),
13092 ));
13093 }
13094 value
13095 .validate_blob_uploader(&commit.author_registration)
13096 .map_err(|error| DbError::Message(error.to_string()))?;
13097 }
13098 validate_prepared_audience_blob_bindings(audiences)
13099 }
13100
13101 fn activate_prepared_write_on(
13102 conn: &Connection,
13103 gates: &Gates,
13104 synced_tables: &[SyncedTable],
13105 write_id: &WriteId,
13106 commit: &StoreBatchCommit,
13107 commit_ref: &StoreBatchCommitRef,
13108 local_cleanup: StoreBatchLocalCleanup,
13109 additional_object_ids: &[ObjectHash],
13110 ) -> Result<(), DbError> {
13111 commit_ref
13112 .verify_commit(commit)
13113 .map_err(|error| DbError::Message(error.to_string()))?;
13114 let audiences = load_prepared_audience_objects_on(conn, write_id)?;
13115 for package in &audiences.packages {
13116 package
13117 .package()
13118 .validate_blob_uploader(&commit.author_registration)
13119 .map_err(|error| DbError::Message(error.to_string()))?;
13120 }
13121 let mut object_ids = std::collections::BTreeSet::new();
13122 object_ids.insert(remote_object_id(&commit_ref.object));
13123 object_ids.extend(
13124 audiences
13125 .packages
13126 .iter()
13127 .map(PreparedAudiencePackage::remote_object_id),
13128 );
13129 object_ids.extend(
13130 audiences
13131 .blobs
13132 .iter()
13133 .map(PreparedAudienceBlob::remote_object_id),
13134 );
13135 object_ids.extend(additional_object_ids.iter().copied());
13136 for object_id in object_ids {
13137 let remote = load_remote_object_on(conn, object_id)?
13138 .into_activated(commit_ref)
13139 .map_err(|error| {
13140 DbError::Message(format!("activate remote object {object_id}: {error}"))
13141 })?;
13142 let state = serde_json::to_string(&remote).map_err(|error| {
13143 DbError::Message(format!("serialize activated remote object: {error}"))
13144 })?;
13145 let updated = conn
13146 .execute(
13147 "UPDATE remote_objects SET state = ?2 WHERE object_id = ?1",
13148 (object_id.to_string(), state),
13149 )
13150 .map_err(DbError::from)?;
13151 if updated != 1 {
13152 return Err(DbError::Message(format!(
13153 "remote object {object_id} disappeared during activation"
13154 )));
13155 }
13156 }
13157 let activation = BlobActivation {
13158 coord: commit_ref.coord.clone(),
13159 };
13160 let mut consumed_uploads = 0;
13161 for package in &audiences.packages {
13162 let winning_rows = crate::sync::apply::current_winning_rows(
13163 conn,
13164 synced_tables,
13165 package.package().changeset(),
13166 )?;
13167 Self::install_winning_blob_bindings_on(
13168 conn,
13169 gates,
13170 synced_tables,
13171 package.package(),
13172 &activation,
13173 &winning_rows,
13174 )?;
13175 for binding in package.package().blob_bindings() {
13176 if consume_created_upload_handoff_on(conn, package.package(), binding)? {
13177 consumed_uploads += 1;
13178 }
13179 }
13180 }
13181 match Self::make_remote_publication_root_on(conn, write_id)? {
13182 Some((root_table, root_id)) => {
13183 if consumed_uploads == 0 {
13184 return Err(DbError::Message(format!(
13185 "make_remote publication {write_id} for {root_table:?}/{root_id:?} contains no Created upload handoff"
13186 )));
13187 }
13188 let remaining: i64 = conn
13189 .query_row(
13190 "SELECT COUNT(*) FROM cloud_outbox
13191 WHERE operation = 'upload' AND root_table = ?1 AND root_id = ?2",
13192 (&root_table, &root_id),
13193 |row| row.get(0),
13194 )
13195 .map_err(DbError::from)?;
13196 if remaining != 0 {
13197 return Err(DbError::Message(format!(
13198 "make_remote publication {write_id} left {remaining} upload handoff(s) for {root_table:?}/{root_id:?}"
13199 )));
13200 }
13201 Self::complete_make_remote_publication_on(conn, write_id)?;
13202 }
13203 None if consumed_uploads != 0 => {
13204 return Err(DbError::Message(format!(
13205 "Store write {write_id} consumed Created upload handoffs without a make_remote publication intent"
13206 )));
13207 }
13208 None => {}
13209 }
13210 Self::record_materialized_commit_on(conn, commit, commit_ref)?;
13211 for drop in local_cleanup.drops {
13212 conn.execute(
13213 "INSERT INTO published_blob_drop_intents
13214 (seq, namespace, blob_id, size, disposition)
13215 VALUES (?1, ?2, ?3, ?4, ?5)
13216 ON CONFLICT(seq, namespace, blob_id) DO NOTHING",
13217 rusqlite::params![
13218 Self::sequence_to_sqlite(
13219 &match &commit_ref.coord {
13220 StoreCommitCoord::MergeConcurrent { stream_id, .. } => {
13221 stream_id.to_string()
13222 }
13223 StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
13224 },
13225 commit_ref.coord.sequence(),
13226 )?,
13227 drop.namespace,
13228 drop.id,
13229 i64::try_from(drop.size).map_err(|_| DbError::Message(
13230 "outbound local cleanup size exceeds SQLite integer".to_string()
13231 ))?,
13232 drop.disposition.as_db(),
13233 ],
13234 )
13235 .map_err(DbError::from)?;
13236 }
13237 conn.execute(
13238 "DELETE FROM store_write_packages WHERE write_id = ?1",
13239 [write_id.as_str()],
13240 )
13241 .map_err(DbError::from)?;
13242 conn.execute(
13243 "DELETE FROM store_write_blobs WHERE write_id = ?1",
13244 [write_id.as_str()],
13245 )
13246 .map_err(DbError::from)?;
13247 conn.execute(
13248 "DELETE FROM store_write_blob_leases WHERE write_id = ?1",
13249 [write_id.as_str()],
13250 )
13251 .map_err(DbError::from)?;
13252 Ok(())
13253 }
13254
13255 #[cfg(test)]
13256 pub(crate) async fn prepared_audience_objects(
13257 &self,
13258 write_id: &WriteId,
13259 ) -> Result<PreparedAudienceObjects, DbError> {
13260 let write_id = write_id.clone();
13261 let loaded = self
13262 .call(move |conn| load_prepared_audience_objects_on(conn, &write_id))
13263 .await?;
13264
13265 let mut verified_blobs = Vec::with_capacity(loaded.blobs.len());
13266 for prepared in loaded.blobs {
13267 if let Some(spool_path) = prepared.spool_path() {
13268 crate::local_blob::verify_exact_file(prepared.blob().object(), spool_path)
13269 .await
13270 .map_err(|error| DbError::Message(format!("prepared blob spool: {error}")))?;
13271 }
13272 verified_blobs.push(prepared);
13273 }
13274 Ok(PreparedAudienceObjects {
13275 packages: loaded.packages,
13276 blobs: verified_blobs,
13277 })
13278 }
13279
13280 pub(crate) async fn prepared_remote_objects(
13281 &self,
13282 write_id: &WriteId,
13283 ) -> Result<Vec<PreparedRemoteObject>, DbError> {
13284 let write_id = write_id.clone();
13285 self.call(move |conn| {
13286 let mut statement = conn
13287 .prepare(
13288 "SELECT remote_object_id, NULL AS spool_path
13289 FROM store_write_packages WHERE write_id = ?1
13290 UNION
13291 SELECT remote_object_id, spool_path
13292 FROM store_write_blobs WHERE write_id = ?1
13293 ORDER BY remote_object_id",
13294 )
13295 .map_err(DbError::from)?;
13296 let ids = statement
13297 .query_map([write_id.as_str()], |row| {
13298 Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
13299 })
13300 .map_err(DbError::from)?
13301 .collect::<Result<Vec<_>, _>>()
13302 .map_err(DbError::from)?;
13303 ids.into_iter()
13304 .map(|(encoded, spool_path)| {
13305 let id = encoded.parse().map_err(|error| {
13306 DbError::Message(format!("prepared remote object id: {error}"))
13307 })?;
13308 Ok(PreparedRemoteObject {
13309 record: load_remote_object_on(conn, id)?,
13310 spool_path: spool_path.map(PathBuf::from),
13311 })
13312 })
13313 .collect()
13314 })
13315 .await
13316 }
13317
13318 pub(crate) async fn mark_remote_object_uploaded(
13319 &self,
13320 expected: RemoteObjectRecord,
13321 ) -> Result<RemoteObjectRecord, DbError> {
13322 self.call(move |conn| mark_remote_object_uploaded_on(conn, expected))
13323 .await
13324 }
13325
13326 pub(crate) async fn mark_candidate_commit_uploaded(
13327 &self,
13328 commit: StoreBatchCommitRef,
13329 ) -> Result<(), DbError> {
13330 self.call(move |conn| {
13331 let object_id = remote_object_id(&commit.object);
13332 let current = load_remote_object_on(conn, object_id)?;
13333 if !matches!(
13334 ¤t,
13335 RemoteObjectRecord::CandidateCommit(record) if record.identity == commit
13336 ) {
13337 return Err(DbError::Message(format!(
13338 "remote object {object_id} is not the exact candidate commit"
13339 )));
13340 }
13341 mark_remote_object_uploaded_on(conn, current)?;
13342 Ok(())
13343 })
13344 .await
13345 }
13346
13347 pub(crate) async fn mark_store_head_uploaded(
13348 &self,
13349 head: crate::sync::store_commit::StoreDeviceHeadRef,
13350 ) -> Result<(), DbError> {
13351 self.call(move |conn| {
13352 let object_id = remote_object_id(&head.object);
13353 let current = load_remote_object_on(conn, object_id)?;
13354 if !matches!(
13355 ¤t,
13356 RemoteObjectRecord::RetainedAuthority(record)
13357 if matches!(
13358 &record.identity.domain,
13359 crate::sync::remote_object::RetainedAuthorityObjectDomain::StoreDeviceHead {
13360 reference
13361 } if reference == &head
13362 )
13363 ) {
13364 return Err(DbError::Message(format!(
13365 "remote object {object_id} is not the exact prepared Store head"
13366 )));
13367 }
13368 mark_remote_object_uploaded_on(conn, current)?;
13369 Ok(())
13370 })
13371 .await
13372 }
13373
13374 pub(crate) async fn blocked_merge_candidate(
13375 &self,
13376 write_id: WriteId,
13377 ) -> Result<Option<BlockedMergeCandidate>, DbError> {
13378 self.call(move |conn| {
13379 let row: Option<(String, Option<String>)> = conn
13380 .query_row(
13381 "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
13382 [write_id.as_str()],
13383 |row| Ok((row.get(0)?, row.get(1)?)),
13384 )
13385 .optional()
13386 .map_err(DbError::from)?;
13387 let Some((raw_status, raw_prepared)) = row else {
13388 return Err(DbError::Message(format!("write {write_id} is absent")));
13389 };
13390 let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
13391 DbError::Message(format!("blocked Merge candidate status: {error}"))
13392 })?;
13393 if !matches!(status, WriteStatus::Blocked(_)) {
13394 return Err(DbError::Message(format!("write {write_id} is not blocked")));
13395 }
13396 let Some(raw_prepared) = raw_prepared else {
13397 return Ok(None);
13398 };
13399 let prepared: PreparedStoreWriteState =
13400 serde_json::from_str(&raw_prepared).map_err(|error| {
13401 DbError::Message(format!("blocked Merge candidate preparation: {error}"))
13402 })?;
13403 let PreparedStoreWriteState::MergeConcurrent { .. } = &prepared else {
13404 return match prepared {
13405 PreparedStoreWriteState::MergeAbandonment { .. } => Ok(None),
13406 PreparedStoreWriteState::SerialPreparing
13407 | PreparedStoreWriteState::Serial { .. } => Err(DbError::Message(
13408 "Serial state reached Merge candidate abandonment".to_string(),
13409 )),
13410 PreparedStoreWriteState::MergeConcurrent { .. } => unreachable!(),
13411 };
13412 };
13413 let candidate = parse_prepared_merge_candidate_on(conn, &prepared)?
13414 .expect("matched Merge preparation");
13415 if candidate.commit.write_id != write_id {
13416 return Err(DbError::Message(
13417 "blocked Merge candidate differs from its write identity".to_string(),
13418 ));
13419 }
13420 Ok(Some(BlockedMergeCandidate {
13421 commit: ExactProtocolObject {
13422 value: candidate.commit,
13423 bytes: candidate.canonical_signed_bytes,
13424 object: candidate.reference.object.clone(),
13425 prepared: match &prepared {
13426 PreparedStoreWriteState::MergeConcurrent { commit, .. } => {
13427 commit.prepared.clone()
13428 }
13429 _ => unreachable!("matched Merge preparation"),
13430 },
13431 },
13432 head: ExactProtocolObject {
13433 value: candidate.head,
13434 bytes: match &prepared {
13435 PreparedStoreWriteState::MergeConcurrent { head, .. } => {
13436 head.semantic_bytes.clone()
13437 }
13438 _ => unreachable!("matched Merge preparation"),
13439 },
13440 object: candidate.head_prepared.reference().clone(),
13441 prepared: candidate.head_prepared,
13442 },
13443 }))
13444 })
13445 .await
13446 }
13447
13448 #[doc(hidden)]
13449 pub async fn merge_candidate_abandonment_required(
13450 &self,
13451 write_id: &WriteId,
13452 ) -> Result<bool, DbError> {
13453 if !matches!(
13454 self.merge_abandonment_state(write_id).await?,
13455 MergeAbandonmentState::None
13456 ) {
13457 return Ok(true);
13458 }
13459 Ok(self
13460 .blocked_merge_candidate(write_id.clone())
13461 .await?
13462 .is_some())
13463 }
13464
13465 pub(crate) async fn merge_abandonment_state(
13466 &self,
13467 write_id: &WriteId,
13468 ) -> Result<MergeAbandonmentState, DbError> {
13469 let write_id = write_id.clone();
13470 self.call(move |conn| {
13471 let raw_prepared: Option<String> = conn
13472 .query_row(
13473 "SELECT prepared FROM store_writes WHERE write_id = ?1",
13474 [write_id.as_str()],
13475 |row| row.get(0),
13476 )
13477 .map_err(DbError::from)?;
13478 let Some(raw_prepared) = raw_prepared else {
13479 return Ok(MergeAbandonmentState::None);
13480 };
13481 let prepared: PreparedStoreWriteState =
13482 serde_json::from_str(&raw_prepared).map_err(|error| {
13483 DbError::Message(format!("prepared Merge abandonment: {error}"))
13484 })?;
13485 let PreparedStoreWriteState::MergeAbandonment {
13486 candidate_commit,
13487 candidate_head,
13488 outcome,
13489 ..
13490 } = &prepared
13491 else {
13492 return Ok(MergeAbandonmentState::None);
13493 };
13494 let candidate =
13495 parse_prepared_merge_candidate_parts_on(conn, candidate_commit, candidate_head)?;
13496 Ok(match outcome {
13497 MergeAbandonmentOutcome::Prepared => MergeAbandonmentState::Prepared,
13498 MergeAbandonmentOutcome::Accepted { .. } => MergeAbandonmentState::Accepted,
13499 MergeAbandonmentOutcome::Lost { winner_commit, .. }
13500 if winner_commit == &candidate.reference =>
13501 {
13502 MergeAbandonmentState::CandidateWon
13503 }
13504 MergeAbandonmentOutcome::Lost { .. } => MergeAbandonmentState::OtherWon,
13505 })
13506 })
13507 .await
13508 }
13509
13510 pub(crate) async fn resume_winning_merge_candidate(
13511 &self,
13512 write_id: WriteId,
13513 ) -> Result<(), DbError> {
13514 let statuses = self.state.write_statuses.clone();
13515 let notified_write_id = write_id.clone();
13516 self.call(move |conn| {
13517 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
13518 let (raw_status, raw_prepared): (String, String) = tx
13519 .query_row(
13520 "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
13521 [write_id.as_str()],
13522 |row| Ok((row.get(0)?, row.get(1)?)),
13523 )
13524 .map_err(DbError::from)?;
13525 let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
13526 DbError::Message(format!("winning Merge candidate status: {error}"))
13527 })?;
13528 if !matches!(status, WriteStatus::Blocked(_)) {
13529 return Err(DbError::Message(
13530 "winning Merge candidate is not blocked".to_string(),
13531 ));
13532 }
13533 let prepared: PreparedStoreWriteState =
13534 serde_json::from_str(&raw_prepared).map_err(|error| {
13535 DbError::Message(format!("winning Merge candidate preparation: {error}"))
13536 })?;
13537 let PreparedStoreWriteState::MergeAbandonment {
13538 candidate_commit,
13539 candidate_head,
13540 authority_commit,
13541 authority_head,
13542 outcome: MergeAbandonmentOutcome::Lost { winner_commit, .. },
13543 local_cleanup,
13544 completion,
13545 } = prepared
13546 else {
13547 return Err(DbError::Message(
13548 "Merge abandonment did not lose to its prepared candidate".to_string(),
13549 ));
13550 };
13551 let candidate =
13552 parse_prepared_merge_candidate_parts_on(&tx, &candidate_commit, &candidate_head)?;
13553 if winner_commit != candidate.reference {
13554 return Err(DbError::Message(
13555 "Merge abandonment winner is another candidate".to_string(),
13556 ));
13557 }
13558 let authority =
13559 parse_prepared_merge_candidate_parts_on(&tx, &authority_commit, &authority_head)?;
13560 remove_cleaned_merge_authority_on(&tx, &authority)?;
13561 let replacement = PreparedStoreWriteState::MergeConcurrent {
13562 commit: candidate_commit,
13563 head: candidate_head,
13564 local_cleanup,
13565 completion,
13566 };
13567 let replacement = serde_json::to_string(&replacement).map_err(|error| {
13568 DbError::Message(format!("serialize winning Merge candidate: {error}"))
13569 })?;
13570 let publishing = serde_json::to_string(&WriteStatus::Publishing).map_err(|error| {
13571 DbError::Message(format!("serialize winning Merge status: {error}"))
13572 })?;
13573 let updated = tx
13574 .execute(
13575 "UPDATE store_writes SET prepared = ?2, status = ?3
13576 WHERE write_id = ?1 AND prepared = ?4",
13577 rusqlite::params![write_id.as_str(), replacement, publishing, raw_prepared],
13578 )
13579 .map_err(DbError::from)?;
13580 if updated != 1 {
13581 return Err(DbError::Message(
13582 "Merge abandonment changed while restoring its winner".to_string(),
13583 ));
13584 }
13585 tx.commit().map_err(DbError::from)?;
13586 Self::notify_write_status_in(&statuses, ¬ified_write_id, WriteStatus::Publishing);
13587 Ok(())
13588 })
13589 .await
13590 }
13591
13592 pub(crate) async fn finish_lost_merge_abandonment(
13593 &self,
13594 write_id: WriteId,
13595 ) -> Result<(), DbError> {
13596 self.call(move |conn| {
13597 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
13598 let (raw_status, raw_prepared): (String, String) = tx
13599 .query_row(
13600 "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
13601 [write_id.as_str()],
13602 |row| Ok((row.get(0)?, row.get(1)?)),
13603 )
13604 .map_err(DbError::from)?;
13605 let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
13606 DbError::Message(format!("lost Merge abandonment status: {error}"))
13607 })?;
13608 if !matches!(status, WriteStatus::Blocked(_)) {
13609 return Err(DbError::Message(
13610 "lost Merge abandonment is not blocked".to_string(),
13611 ));
13612 }
13613 let prepared: PreparedStoreWriteState =
13614 serde_json::from_str(&raw_prepared).map_err(|error| {
13615 DbError::Message(format!("lost Merge abandonment preparation: {error}"))
13616 })?;
13617 let PreparedStoreWriteState::MergeAbandonment {
13618 candidate_commit,
13619 candidate_head,
13620 authority_commit,
13621 authority_head,
13622 outcome: MergeAbandonmentOutcome::Lost { winner_commit, .. },
13623 local_cleanup,
13624 completion,
13625 } = prepared
13626 else {
13627 return Err(DbError::Message(
13628 "Merge abandonment has no third-candidate winner".to_string(),
13629 ));
13630 };
13631 let candidate =
13632 parse_prepared_merge_candidate_parts_on(&tx, &candidate_commit, &candidate_head)?;
13633 if winner_commit == candidate.reference {
13634 return Err(DbError::Message(
13635 "original candidate won the Merge abandonment race".to_string(),
13636 ));
13637 }
13638 let authority =
13639 parse_prepared_merge_candidate_parts_on(&tx, &authority_commit, &authority_head)?;
13640 remove_cleaned_merge_authority_on(&tx, &authority)?;
13641 let replacement = PreparedStoreWriteState::MergeConcurrent {
13642 commit: candidate_commit,
13643 head: candidate_head,
13644 local_cleanup,
13645 completion,
13646 };
13647 let replacement = serde_json::to_string(&replacement).map_err(|error| {
13648 DbError::Message(format!("serialize lost Merge candidate: {error}"))
13649 })?;
13650 let updated = tx
13651 .execute(
13652 "UPDATE store_writes SET prepared = ?2
13653 WHERE write_id = ?1 AND status = ?3 AND prepared = ?4",
13654 rusqlite::params![write_id.as_str(), replacement, raw_status, raw_prepared],
13655 )
13656 .map_err(DbError::from)?;
13657 if updated != 1 {
13658 return Err(DbError::Message(
13659 "Merge abandonment changed while removing its losing authority".to_string(),
13660 ));
13661 }
13662 tx.commit().map_err(DbError::from)
13663 })
13664 .await
13665 }
13666
13667 #[doc(hidden)]
13668 pub async fn merge_candidate_cleanup_pending(
13669 &self,
13670 write_id: &WriteId,
13671 ) -> Result<bool, DbError> {
13672 let write_id = write_id.clone();
13673 self.call(move |conn| {
13674 let raw_prepared: Option<String> = conn
13675 .query_row(
13676 "SELECT prepared FROM store_writes WHERE write_id = ?1",
13677 [write_id.as_str()],
13678 |row| row.get(0),
13679 )
13680 .map_err(DbError::from)?;
13681 let Some(raw_prepared) = raw_prepared else {
13682 return Ok(false);
13683 };
13684 let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
13685 .map_err(|error| DbError::Message(format!("prepared Merge cleanup: {error}")))?;
13686 let Some(candidate) = parse_prepared_merge_candidate_on(conn, &prepared)? else {
13687 return Ok(false);
13688 };
13689 let cleanup_pending = |candidate: &PreparedMergeCandidate| -> Result<bool, DbError> {
13690 let remote = load_remote_object_on(
13691 conn,
13692 remote_object_id(&candidate.reference.object),
13693 )?;
13694 Ok(matches!(
13695 remote,
13696 RemoteObjectRecord::CandidateCommit(
13697 crate::sync::remote_object::CandidateCommitRecord {
13698 state:
13699 crate::sync::remote_object::CandidateCommitState::CleanupPending {
13700 proof: crate::sync::remote_object::CandidateNonactivationProof::MergeWinner { .. }
13701 },
13702 ..
13703 }
13704 )
13705 ))
13706 };
13707 match &prepared {
13708 PreparedStoreWriteState::MergeConcurrent { .. } => cleanup_pending(&candidate),
13709 PreparedStoreWriteState::MergeAbandonment {
13710 outcome,
13711 authority_commit,
13712 authority_head,
13713 ..
13714 } => {
13715 let authority = parse_prepared_merge_candidate_parts_on(
13716 conn,
13717 authority_commit,
13718 authority_head,
13719 )?;
13720 match outcome {
13721 MergeAbandonmentOutcome::Prepared => Ok(false),
13722 MergeAbandonmentOutcome::Accepted { .. } => cleanup_pending(&candidate),
13723 MergeAbandonmentOutcome::Lost { winner_commit, .. } => Ok(
13724 (winner_commit != &candidate.reference && cleanup_pending(&candidate)?)
13725 || cleanup_pending(&authority)?,
13726 ),
13727 }
13728 }
13729 PreparedStoreWriteState::SerialPreparing
13730 | PreparedStoreWriteState::Serial { .. } => Ok(false),
13731 }
13732 })
13733 .await
13734 }
13735
13736 pub(crate) async fn merge_candidate_cleanup_targets(
13737 &self,
13738 write_id: WriteId,
13739 ) -> Result<Vec<CandidateCleanupObject>, DbError> {
13740 self.call(move |conn| {
13741 let (raw_status, raw_prepared): (String, String) = conn
13742 .query_row(
13743 "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
13744 [write_id.as_str()],
13745 |row| Ok((row.get(0)?, row.get(1)?)),
13746 )
13747 .map_err(DbError::from)?;
13748 let status: WriteStatus = serde_json::from_str(&raw_status)
13749 .map_err(|error| DbError::Message(format!("Merge cleanup status: {error}")))?;
13750 if !matches!(status, WriteStatus::Blocked(_)) {
13751 return Err(DbError::Message(format!(
13752 "Merge cleanup write {write_id} is not blocked"
13753 )));
13754 }
13755 let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
13756 .map_err(|error| DbError::Message(format!("prepared Merge cleanup: {error}")))?;
13757 let Some(candidate) = parse_prepared_merge_candidate_on(conn, &prepared)? else {
13758 return Err(DbError::Message(
13759 "Serial state reached Merge candidate cleanup".to_string(),
13760 ));
13761 };
13762 match &prepared {
13763 PreparedStoreWriteState::MergeConcurrent { .. } => {
13764 merge_candidate_cleanup_targets_on(conn, &write_id, &candidate, true)
13765 }
13766 PreparedStoreWriteState::MergeAbandonment {
13767 outcome,
13768 authority_commit,
13769 authority_head,
13770 ..
13771 } => {
13772 let authority = parse_prepared_merge_candidate_parts_on(
13773 conn,
13774 authority_commit,
13775 authority_head,
13776 )?;
13777 let mut targets = Vec::new();
13778 match outcome {
13779 MergeAbandonmentOutcome::Prepared => {
13780 return Err(DbError::Message(
13781 "Merge abandonment has no accepted winner".to_string(),
13782 ));
13783 }
13784 MergeAbandonmentOutcome::Accepted { .. } => {
13785 targets.extend(merge_candidate_cleanup_targets_on(
13786 conn, &write_id, &candidate, true,
13787 )?);
13788 }
13789 MergeAbandonmentOutcome::Lost { winner_commit, .. } => {
13790 if winner_commit != &candidate.reference {
13791 targets.extend(merge_candidate_cleanup_targets_on(
13792 conn, &write_id, &candidate, true,
13793 )?);
13794 }
13795 targets.extend(merge_candidate_cleanup_targets_on(
13796 conn, &write_id, &authority, false,
13797 )?);
13798 }
13799 }
13800 Ok(targets)
13801 }
13802 PreparedStoreWriteState::SerialPreparing
13803 | PreparedStoreWriteState::Serial { .. } => unreachable!("parsed Merge cleanup"),
13804 }
13805 })
13806 .await
13807 }
13808
13809 pub(crate) async fn adopt_alternate_merge_head(
13810 &self,
13811 write_id: WriteId,
13812 winner: StoreDeviceHead,
13813 winner_prepared: PreparedExactObject,
13814 ) -> Result<(), DbError> {
13815 self.call(move |conn| {
13816 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
13817 let (raw_status, raw_prepared): (String, String) = tx
13818 .query_row(
13819 "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
13820 [write_id.as_str()],
13821 |row| Ok((row.get(0)?, row.get(1)?)),
13822 )
13823 .map_err(DbError::from)?;
13824 let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
13825 DbError::Message(format!("alternate Merge head status: {error}"))
13826 })?;
13827 if !matches!(status, WriteStatus::Publishing) {
13828 return Err(DbError::Message(format!(
13829 "Merge candidate {write_id} is not publishing"
13830 )));
13831 }
13832 let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
13833 .map_err(|error| DbError::Message(format!("prepared Merge candidate: {error}")))?;
13834 let publication = parse_prepared_merge_publication_on(&tx, &prepared)?
13835 .ok_or_else(|| {
13836 DbError::Message(
13837 "Serial branch reached alternate Merge head adoption".to_string(),
13838 )
13839 })?;
13840 if winner_prepared.reference().slot()
13841 != publication.head_prepared.reference().slot()
13842 || winner_prepared.reference() == publication.head_prepared.reference()
13843 {
13844 return Err(DbError::Message(
13845 "alternate Merge head does not replace the prepared exact slot".to_string(),
13846 ));
13847 }
13848 let root = required_store_root_authority_on(&tx)?;
13849 let registration = load_activated_registration_on(
13850 &tx,
13851 &root,
13852 &publication.commit.author_registration,
13853 )?;
13854 let candidate = publication.reference;
13855 let verified_winner = StoreDeviceHead::parse_at(
13856 &winner.to_bytes(),
13857 root.store_root_hash,
13858 ®istration,
13859 &candidate,
13860 )
13861 .map_err(|error| DbError::Message(format!("verify alternate Merge head: {error}")))?;
13862 if verified_winner != winner || winner.commit != candidate {
13863 return Err(DbError::Message(
13864 "alternate Merge head does not activate the prepared commit".to_string(),
13865 ));
13866 }
13867 let old_object_id = remote_object_id(publication.head_prepared.reference());
13868 let old_remote = load_remote_object_on(&tx, old_object_id)?;
13869 if !matches!(
13870 &old_remote,
13871 RemoteObjectRecord::RetainedAuthority(record)
13872 if matches!(
13873 &record.identity.domain,
13874 crate::sync::remote_object::RetainedAuthorityObjectDomain::StoreDeviceHead { .. }
13875 ) && matches!(
13876 &record.state,
13877 crate::sync::remote_object::RetainedAuthorityObjectState::Prepared {
13878 ownership
13879 } if ownership.pending == BTreeSet::from([candidate.clone()])
13880 )
13881 ) {
13882 return Err(DbError::Message(
13883 "prepared Merge head lost its candidate ownership".to_string(),
13884 ));
13885 }
13886 let removed = tx
13887 .execute(
13888 "DELETE FROM remote_objects WHERE object_id = ?1",
13889 [old_object_id.to_string()],
13890 )
13891 .map_err(DbError::from)?;
13892 if removed != 1 {
13893 return Err(DbError::Message(
13894 "prepared Merge head disappeared during replacement".to_string(),
13895 ));
13896 }
13897 let winner_ref = crate::sync::store_commit::StoreDeviceHeadRef {
13898 head_hash: winner.head_hash(),
13899 object: winner_prepared.reference().clone(),
13900 };
13901 let mut winner_remote = RemoteObjectRecord::candidate_activated_store_head(
13902 winner_ref,
13903 winner.to_bytes(),
13904 winner_prepared.stored_bytes().to_vec(),
13905 candidate,
13906 )
13907 .map_err(|error| DbError::Message(format!("alternate Merge head: {error}")))?;
13908 winner_remote.mark_uploaded_verified().map_err(|error| {
13909 DbError::Message(format!("mark alternate Merge head uploaded: {error}"))
13910 })?;
13911 persist_exact_remote_object_on(&tx, &winner_remote, "alternate Merge head")?;
13912 let replacement_head = DurablePreparedProtocolObject {
13913 semantic_bytes: winner.to_bytes(),
13914 prepared: winner_prepared,
13915 };
13916 let replacement = match prepared {
13917 PreparedStoreWriteState::MergeConcurrent {
13918 commit,
13919 local_cleanup,
13920 completion,
13921 ..
13922 } => PreparedStoreWriteState::MergeConcurrent {
13923 commit,
13924 head: replacement_head,
13925 local_cleanup,
13926 completion,
13927 },
13928 PreparedStoreWriteState::MergeAbandonment {
13929 candidate_commit,
13930 candidate_head,
13931 authority_commit,
13932 outcome,
13933 local_cleanup,
13934 completion,
13935 ..
13936 } => PreparedStoreWriteState::MergeAbandonment {
13937 candidate_commit,
13938 candidate_head,
13939 authority_commit,
13940 authority_head: replacement_head,
13941 outcome,
13942 local_cleanup,
13943 completion,
13944 },
13945 PreparedStoreWriteState::SerialPreparing
13946 | PreparedStoreWriteState::Serial { .. } => {
13947 unreachable!("parsed Merge publication")
13948 }
13949 };
13950 let replacement = serde_json::to_string(&replacement).map_err(|error| {
13951 DbError::Message(format!("serialize alternate Merge preparation: {error}"))
13952 })?;
13953 let updated = tx
13954 .execute(
13955 "UPDATE store_writes SET prepared = ?2
13956 WHERE write_id = ?1 AND status = '\"publishing\"' AND prepared = ?3",
13957 rusqlite::params![write_id.as_str(), replacement, raw_prepared],
13958 )
13959 .map_err(DbError::from)?;
13960 if updated != 1 {
13961 return Err(DbError::Message(
13962 "prepared Merge write changed during head replacement".to_string(),
13963 ));
13964 }
13965 tx.commit().map_err(DbError::from)
13966 })
13967 .await
13968 }
13969
13970 pub(crate) async fn prepare_serial_candidate_cleanup(
13971 &self,
13972 branch_id: PendingBranchId,
13973 plan: &crate::sync::store_pull::SerialResolutionPlan,
13974 ) -> Result<Vec<CandidateCleanupObject>, DbError> {
13975 let accepted_refs = plan
13976 .commits
13977 .iter()
13978 .map(|commit| commit.commit_ref.clone())
13979 .collect::<Vec<_>>();
13980 let accepted_commits = plan
13981 .commits
13982 .iter()
13983 .map(|commit| commit.commit.clone())
13984 .collect::<Vec<_>>();
13985 let head = plan.head.clone();
13986 let head_bytes = plan.head_object.bytes.clone();
13987 let observed_version_hash =
13988 ObjectHash::digest(plan.head_object.version.cloud().as_provider().as_bytes());
13989 self.call(move |conn| {
13990 if head.to_bytes() != head_bytes {
13991 return Err(DbError::Message(
13992 "Serial cleanup head bytes differ from the verified head".to_string(),
13993 ));
13994 }
13995 let mut statement = conn
13996 .prepare(
13997 "SELECT write_id, base, status, prepared FROM store_writes
13998 WHERE status != '\"local_only\"'
13999 AND json_extract(status, '$.published') IS NULL
14000 AND json_extract(status, '$.resolved') IS NULL
14001 AND json_type(base, '$.serial') IS NOT NULL
14002 ORDER BY ordinal",
14003 )
14004 .map_err(DbError::from)?;
14005 let rows = statement
14006 .query_map([], |row| {
14007 Ok((
14008 row.get::<_, String>(0)?,
14009 row.get::<_, String>(1)?,
14010 row.get::<_, String>(2)?,
14011 row.get::<_, Option<String>>(3)?,
14012 ))
14013 })
14014 .map_err(DbError::from)?
14015 .collect::<Result<Vec<_>, _>>()
14016 .map_err(DbError::from)?;
14017 drop(statement);
14018 let mut branch_base = None;
14019 let mut prepared = Vec::new();
14020 for (write_id, raw_base, raw_status, raw_prepared) in rows {
14021 let base: StoreWriteBase = serde_json::from_str(&raw_base).map_err(|error| {
14022 DbError::Message(format!("Serial cleanup branch base: {error}"))
14023 })?;
14024 let StoreWriteBase::Serial {
14025 branch_id: stored_branch_id,
14026 base,
14027 } = base
14028 else {
14029 return Err(DbError::Message(
14030 "MergeConcurrent base reached Serial candidate cleanup".to_string(),
14031 ));
14032 };
14033 if stored_branch_id != branch_id {
14034 return Err(DbError::Message(
14035 "Serial database contains more than one unresolved branch".to_string(),
14036 ));
14037 }
14038 let status: WriteStatus = serde_json::from_str(&raw_status).map_err(|error| {
14039 DbError::Message(format!("Serial cleanup branch status: {error}"))
14040 })?;
14041 if !matches!(
14042 status,
14043 WriteStatus::Conflict(ref conflict) if conflict.branch_id == branch_id
14044 ) {
14045 return Err(DbError::Message(format!(
14046 "Serial cleanup write {write_id} is not conflicted"
14047 )));
14048 }
14049 if branch_base.as_ref().is_some_and(|stored| stored != &base) {
14050 return Err(DbError::Message(
14051 "Serial cleanup branch has inconsistent bases".to_string(),
14052 ));
14053 }
14054 branch_base.get_or_insert(base);
14055 let Some(raw_prepared) = raw_prepared else {
14056 continue;
14057 };
14058 if let Some(candidate) = parse_prepared_serial_candidate(&raw_prepared)? {
14059 prepared.push((
14060 WriteId::from_generated(write_id),
14061 candidate.commit,
14062 candidate.reference,
14063 candidate.canonical_signed_bytes,
14064 ));
14065 }
14066 }
14067 if prepared.is_empty() {
14068 return Ok(Vec::new());
14069 }
14070 let branch_base = branch_base.expect("prepared branch has a base");
14071 if accepted_refs.is_empty() || accepted_refs.len() != accepted_commits.len() {
14072 return Err(DbError::Message(
14073 "Serial candidate cleanup requires a nonempty accepted suffix".to_string(),
14074 ));
14075 }
14076 let mut accepted_predecessor = branch_base.clone();
14077 for (reference, commit) in accepted_refs.iter().zip(&accepted_commits) {
14078 reference
14079 .verify_commit(commit)
14080 .map_err(|error| DbError::Message(error.to_string()))?;
14081 if commit.order.predecessor() != accepted_predecessor.as_ref() {
14082 return Err(DbError::Message(
14083 "accepted Serial cleanup suffix has a broken exact predecessor"
14084 .to_string(),
14085 ));
14086 }
14087 accepted_predecessor = Some(reference.clone());
14088 }
14089 if !matches!(
14090 head.state,
14091 StoreSerialHeadState::Commit { ref commit, .. }
14092 if Some(commit) == accepted_refs.last()
14093 ) {
14094 return Err(DbError::Message(
14095 "Serial cleanup head does not name the accepted suffix tip".to_string(),
14096 ));
14097 }
14098 let mut losing_predecessor = branch_base.clone();
14099 let mut losing_targets = Vec::with_capacity(prepared.len());
14100 for (_, commit, reference, bytes) in &prepared {
14101 if commit.order.predecessor() != losing_predecessor.as_ref() {
14102 return Err(DbError::Message(
14103 "losing Serial candidates have a broken exact predecessor chain"
14104 .to_string(),
14105 ));
14106 }
14107 losing_targets.push(
14108 crate::sync::remote_object::StoreBatchCommitDeletionTarget {
14109 coord: reference.coord.clone(),
14110 object: reference.object.clone(),
14111 canonical_signed_bytes: bytes.clone(),
14112 },
14113 );
14114 losing_predecessor = Some(reference.clone());
14115 }
14116 if accepted_refs.first() == prepared.first().map(|(_, _, reference, _)| reference) {
14117 return Err(DbError::Message(
14118 "accepted Serial suffix begins with the losing candidate".to_string(),
14119 ));
14120 }
14121 let suffix = crate::sync::remote_object::SerialAcceptedSuffix {
14122 predecessor: branch_base,
14123 commits: accepted_refs,
14124 canonical_signed_head_bytes: head_bytes,
14125 observed_version_hash,
14126 };
14127 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
14128 let mut exclusive_cleanup = Vec::new();
14129 let mut commit_cleanup = Vec::new();
14130 for (index, (write_id, commit, reference, _)) in prepared.iter().enumerate() {
14131 let proof = crate::sync::remote_object::CandidateNonactivationProof::SerialImmediateSuccessor {
14132 accepted_suffix: suffix.clone(),
14133 losing_prefix: losing_targets[..=index].to_vec(),
14134 };
14135 let nonactivation = crate::sync::remote_object::CandidateNonactivation {
14136 candidate: losing_targets[index].clone(),
14137 proof,
14138 };
14139 let mut object_statement = tx
14140 .prepare(
14141 "SELECT remote_object_id FROM store_write_packages WHERE write_id = ?1
14142 UNION
14143 SELECT remote_object_id FROM store_write_blobs WHERE write_id = ?1
14144 ORDER BY remote_object_id",
14145 )
14146 .map_err(DbError::from)?;
14147 let object_ids = object_statement
14148 .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
14149 .map_err(DbError::from)?
14150 .collect::<Result<Vec<_>, _>>()
14151 .map_err(DbError::from)?;
14152 drop(object_statement);
14153 let mut manifest_cleanup = BTreeSet::new();
14154 for encoded in object_ids {
14155 let object_id: ObjectHash = encoded.parse().map_err(|error| {
14156 DbError::Message(format!("Serial cleanup remote object id: {error}"))
14157 })?;
14158 let mut remote = load_remote_object_on(&tx, object_id)?;
14159 remote
14160 .begin_candidate_nonactivation(nonactivation.clone())
14161 .map_err(|error| {
14162 DbError::Message(format!(
14163 "record candidate nonactivation for {object_id}: {error}"
14164 ))
14165 })?;
14166 if let Some(object) = remote.cleanup_target() {
14167 manifest_cleanup.insert(object.clone());
14168 }
14169 update_remote_object_on(&tx, object_id, &remote)?;
14170 }
14171 for object in candidate_manifest_exact_objects(commit) {
14172 if manifest_cleanup.remove(object) {
14173 exclusive_cleanup.push(CandidateCleanupObject {
14174 object: object.clone(),
14175 });
14176 }
14177 }
14178 if !manifest_cleanup.is_empty() {
14179 return Err(DbError::Message(format!(
14180 "candidate cleanup for write {write_id} contains an object outside its signed manifest"
14181 )));
14182 }
14183 let commit_object_id = remote_object_id(&reference.object);
14184 let mut remote = load_remote_object_on(&tx, commit_object_id)?;
14185 remote
14186 .begin_candidate_nonactivation(nonactivation)
14187 .map_err(|error| {
14188 DbError::Message(format!(
14189 "record candidate commit nonactivation for {commit_object_id}: {error}"
14190 ))
14191 })?;
14192 if let Some(object) = remote.cleanup_target() {
14193 commit_cleanup.push(CandidateCleanupObject {
14194 object: object.clone(),
14195 });
14196 }
14197 update_remote_object_on(&tx, commit_object_id, &remote)?;
14198 }
14199 tx.commit().map_err(DbError::from)?;
14200 exclusive_cleanup.extend(commit_cleanup);
14201 Ok(exclusive_cleanup)
14202 })
14203 .await
14204 }
14205
14206 pub(crate) async fn prepare_serial_abandonment_authority_cleanup(
14207 &self,
14208 plan: &crate::sync::store_pull::SerialResolutionPlan,
14209 ) -> Result<Option<CandidateCleanupObject>, DbError> {
14210 let prepared = self
14211 .prepared_serial_candidate_abandonment()
14212 .await?
14213 .ok_or_else(|| {
14214 DbError::Message("Serial candidate abandonment is not prepared".to_string())
14215 })?;
14216 let accepted_refs = plan
14217 .commits
14218 .iter()
14219 .map(|commit| commit.commit_ref.clone())
14220 .collect::<Vec<_>>();
14221 if accepted_refs.is_empty() {
14222 return Err(DbError::Message(
14223 "Serial abandonment cleanup requires an accepted successor".to_string(),
14224 ));
14225 }
14226 let authority_ref = StoreBatchCommitRef::from_commit(
14227 &prepared.authority.value,
14228 StoreCommitCoord::Serial {
14229 sequence: prepared.authority.value.seq(),
14230 },
14231 prepared.authority.object.clone(),
14232 )
14233 .map_err(|error| DbError::Message(error.to_string()))?;
14234 if accepted_refs.first() == Some(&authority_ref) {
14235 return Ok(None);
14236 }
14237 let head_bytes = plan.head_object.bytes.clone();
14238 if plan.head.to_bytes() != head_bytes
14239 || !matches!(
14240 &plan.head.state,
14241 StoreSerialHeadState::Commit { commit, .. }
14242 if Some(commit) == accepted_refs.last()
14243 )
14244 {
14245 return Err(DbError::Message(
14246 "Serial abandonment cleanup head differs from its accepted suffix".to_string(),
14247 ));
14248 }
14249 let mut predecessor = prepared.base.clone();
14250 for commit in &plan.commits {
14251 commit
14252 .commit_ref
14253 .verify_commit(&commit.commit)
14254 .map_err(|error| DbError::Message(error.to_string()))?;
14255 if commit.commit.order.predecessor() != predecessor.as_ref() {
14256 return Err(DbError::Message(
14257 "Serial abandonment accepted suffix has a broken predecessor".to_string(),
14258 ));
14259 }
14260 predecessor = Some(commit.commit_ref.clone());
14261 }
14262 let authority_target = crate::sync::store_commit::StoreBatchCommitDeletionTarget {
14263 coord: authority_ref.coord.clone(),
14264 object: authority_ref.object.clone(),
14265 canonical_signed_bytes: prepared.authority.bytes.clone(),
14266 };
14267 let suffix = crate::sync::remote_object::SerialAcceptedSuffix {
14268 predecessor: prepared.base,
14269 commits: accepted_refs,
14270 canonical_signed_head_bytes: head_bytes,
14271 observed_version_hash: ObjectHash::digest(
14272 plan.head_object.version.cloud().as_provider().as_bytes(),
14273 ),
14274 };
14275 let expected_state = prepared.durable_state;
14276 self.call(move |conn| {
14277 let state_matches: bool = conn
14278 .query_row(
14279 "SELECT EXISTS(
14280 SELECT 1 FROM protocol_state WHERE key = ?1 AND value = ?2
14281 )",
14282 rusqlite::params![SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY, expected_state],
14283 |row| row.get(0),
14284 )
14285 .map_err(DbError::from)?;
14286 if !state_matches {
14287 return Err(DbError::Message(
14288 "Serial abandonment durable state changed before authority cleanup"
14289 .to_string(),
14290 ));
14291 }
14292 let object_id = remote_object_id(&authority_ref.object);
14293 let mut remote = load_remote_object_on(conn, object_id)?;
14294 remote
14295 .begin_candidate_nonactivation(
14296 crate::sync::remote_object::CandidateNonactivation {
14297 candidate: authority_target.clone(),
14298 proof: crate::sync::remote_object::CandidateNonactivationProof::SerialImmediateSuccessor {
14299 accepted_suffix: suffix,
14300 losing_prefix: vec![authority_target],
14301 },
14302 },
14303 )
14304 .map_err(|error| {
14305 DbError::Message(format!(
14306 "record Serial abandonment nonactivation for {object_id}: {error}"
14307 ))
14308 })?;
14309 let target = remote.cleanup_target().cloned();
14310 update_remote_object_on(conn, object_id, &remote)?;
14311 Ok(target.map(|object| CandidateCleanupObject { object }))
14312 })
14313 .await
14314 }
14315
14316 pub(crate) async fn discard_serial_branch_after_abandonment(
14317 &self,
14318 branch_id: PendingBranchId,
14319 plan: crate::sync::store_pull::SerialResolutionPlan,
14320 ) -> Result<(), DbError> {
14321 let prepared = self
14322 .prepared_serial_candidate_abandonment()
14323 .await?
14324 .ok_or_else(|| {
14325 DbError::Message("Serial candidate abandonment is not prepared".to_string())
14326 })?;
14327 if prepared.branch_id != branch_id {
14328 return Err(DbError::Message(
14329 "Serial abandonment belongs to another branch".to_string(),
14330 ));
14331 }
14332 let authority_ref = StoreBatchCommitRef::from_commit(
14333 &prepared.authority.value,
14334 StoreCommitCoord::Serial {
14335 sequence: prepared.authority.value.seq(),
14336 },
14337 prepared.authority.object.clone(),
14338 )
14339 .map_err(|error| DbError::Message(error.to_string()))?;
14340 let authority_accepted = plan
14341 .commits
14342 .first()
14343 .is_some_and(|commit| commit.commit_ref == authority_ref);
14344 let expected_state = prepared.durable_state;
14345 let synced_tables = self.synced_tables().to_vec();
14346 let statuses = self.state.write_statuses.clone();
14347 self.call(move |conn| {
14348 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
14349 let write_ids =
14350 Self::apply_serial_resolution_on(&tx, &synced_tables, &branch_id, plan)?;
14351 let object_id = remote_object_id(&authority_ref.object);
14352 let remote = load_remote_object_on(&tx, object_id)?;
14353 if authority_accepted {
14354 let retained = remote.into_activated(&authority_ref).map_err(|error| {
14355 DbError::Message(format!("activate Serial abandonment authority: {error}"))
14356 })?;
14357 update_remote_object_on(&tx, object_id, &retained)?;
14358 } else {
14359 if !remote
14360 .candidate_cleanup_complete(&authority_ref)
14361 .map_err(|error| {
14362 DbError::Message(format!(
14363 "validate Serial abandonment authority cleanup: {error}"
14364 ))
14365 })?
14366 {
14367 return Err(DbError::Message(
14368 "Serial abandonment authority cleanup is incomplete".to_string(),
14369 ));
14370 }
14371 let removed = tx
14372 .execute(
14373 "DELETE FROM remote_objects WHERE object_id = ?1",
14374 [object_id.to_string()],
14375 )
14376 .map_err(DbError::from)?;
14377 if removed != 1 {
14378 return Err(DbError::Message(
14379 "Serial abandonment authority disappeared during removal".to_string(),
14380 ));
14381 }
14382 }
14383 let removed = tx
14384 .execute(
14385 "DELETE FROM protocol_state WHERE key = ?1 AND value = ?2",
14386 rusqlite::params![SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY, expected_state],
14387 )
14388 .map_err(DbError::from)?;
14389 if removed != 1 {
14390 return Err(DbError::Message(
14391 "Serial abandonment durable state disappeared".to_string(),
14392 ));
14393 }
14394 let resolution = WriteResolution::Discarded;
14395 Self::resolve_unpublished_writes_on(&tx, &write_ids, &resolution)?;
14396 tx.commit().map_err(DbError::from)?;
14397 let status = WriteStatus::Resolved(resolution);
14398 for write_id in write_ids {
14399 Self::notify_write_status_in(&statuses, &write_id, status.clone());
14400 }
14401 Ok(())
14402 })
14403 .await
14404 }
14405
14406 pub(crate) async fn remove_losing_serial_abandonment_authority(&self) -> Result<(), DbError> {
14407 let prepared = self
14408 .prepared_serial_candidate_abandonment()
14409 .await?
14410 .ok_or_else(|| {
14411 DbError::Message("Serial candidate abandonment is not prepared".to_string())
14412 })?;
14413 let authority_ref = StoreBatchCommitRef::from_commit(
14414 &prepared.authority.value,
14415 StoreCommitCoord::Serial {
14416 sequence: prepared.authority.value.seq(),
14417 },
14418 prepared.authority.object.clone(),
14419 )
14420 .map_err(|error| DbError::Message(error.to_string()))?;
14421 let expected_state = prepared.durable_state;
14422 self.call(move |conn| {
14423 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
14424 let object_id = remote_object_id(&authority_ref.object);
14425 let remote = load_remote_object_on(&tx, object_id)?;
14426 if !remote
14427 .candidate_cleanup_complete(&authority_ref)
14428 .map_err(|error| {
14429 DbError::Message(format!(
14430 "validate losing Serial abandonment cleanup: {error}"
14431 ))
14432 })?
14433 {
14434 return Err(DbError::Message(
14435 "losing Serial abandonment cleanup is incomplete".to_string(),
14436 ));
14437 }
14438 let removed = tx
14439 .execute(
14440 "DELETE FROM remote_objects WHERE object_id = ?1",
14441 [object_id.to_string()],
14442 )
14443 .map_err(DbError::from)?;
14444 if removed != 1 {
14445 return Err(DbError::Message(
14446 "losing Serial abandonment authority disappeared".to_string(),
14447 ));
14448 }
14449 let removed = tx
14450 .execute(
14451 "DELETE FROM protocol_state WHERE key = ?1 AND value = ?2",
14452 rusqlite::params![SERIAL_CANDIDATE_ABANDONMENT_STATE_KEY, expected_state],
14453 )
14454 .map_err(DbError::from)?;
14455 if removed != 1 {
14456 return Err(DbError::Message(
14457 "Serial abandonment durable state disappeared".to_string(),
14458 ));
14459 }
14460 tx.commit().map_err(DbError::from)
14461 })
14462 .await
14463 }
14464
14465 pub(crate) async fn mark_candidate_cleanup_absent(
14466 &self,
14467 object: ExactObjectRef,
14468 ) -> Result<(), DbError> {
14469 self.call(move |conn| {
14470 let object_id = remote_object_id(&object);
14471 let mut remote = load_remote_object_on(conn, object_id)?;
14472 if remote.cleanup_target() != Some(&object) {
14473 return Err(DbError::Message(format!(
14474 "remote object {object_id} is not awaiting exact cleanup"
14475 )));
14476 }
14477 remote.mark_absent_verified().map_err(|error| {
14478 DbError::Message(format!("mark candidate {object_id} absent: {error}"))
14479 })?;
14480 update_remote_object_on(conn, object_id, &remote)
14481 })
14482 .await
14483 }
14484
14485 pub(crate) fn install_winning_blob_bindings_on(
14489 conn: &Connection,
14490 gates: &Gates,
14491 synced_tables: &[SyncedTable],
14492 package: &AudiencePackage,
14493 activation: &BlobActivation,
14494 winning_rows: &[crate::sync::apply::WinningRow],
14495 ) -> Result<usize, DbError> {
14496 if package.commit_coord() != &activation.coord {
14497 return Err(DbError::Message(format!(
14498 "blob activation {:?} does not match audience package {:?}",
14499 activation.coord,
14500 package.commit_coord()
14501 )));
14502 }
14503
14504 let package_audience = package.audience().remote_audience();
14505 for winner in winning_rows {
14506 let Some(table) = synced_tables
14507 .iter()
14508 .find(|table| table.name() == winner.table)
14509 else {
14510 return Err(DbError::Message(format!(
14511 "winning changeset row names undeclared table {:?}",
14512 winner.table
14513 )));
14514 };
14515 let Some(declaration) = table.blob() else {
14516 continue;
14517 };
14518 match winner.row_stamp.as_deref() {
14519 Some(row_stamp) => conn.execute(
14520 "DELETE FROM row_blob_locators
14521 WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3
14522 AND row_stamp <> ?4",
14523 rusqlite::params![
14524 winner.table,
14525 winner.row_id,
14526 declaration.id_column,
14527 row_stamp,
14528 ],
14529 ),
14530 None => conn.execute(
14531 "DELETE FROM row_blob_locators
14532 WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3",
14533 rusqlite::params![winner.table, winner.row_id, declaration.id_column],
14534 ),
14535 }
14536 .map_err(DbError::from)?;
14537 }
14538 let mut installed = 0;
14539 for binding in package.blob_bindings() {
14540 let Some(table) = synced_tables
14541 .iter()
14542 .find(|table| table.name() == binding.table())
14543 else {
14544 return Err(DbError::Message(format!(
14545 "blob binding names undeclared table {:?}",
14546 binding.table()
14547 )));
14548 };
14549 let declaration = table.blob().ok_or_else(|| {
14550 DbError::Message(format!(
14551 "blob binding names table {:?}, which has no blob declaration",
14552 binding.table()
14553 ))
14554 })?;
14555 if binding.column() != declaration.id_column {
14556 return Err(DbError::Message(format!(
14557 "blob binding column {:?} does not match declared blob-id column {:?} on table {:?}",
14558 binding.column(), declaration.id_column, binding.table()
14559 )));
14560 }
14561
14562 let Some(row) = live_blob_row(conn, binding.table(), binding.row_id(), declaration)?
14563 else {
14564 continue;
14565 };
14566 if row.stamp != binding.row_stamp() {
14567 continue;
14568 }
14569
14570 let live_audience =
14571 gate::live_row_audience(conn, gates, binding.table(), binding.row_id()).map_err(
14572 |error| {
14573 DbError::Message(format!(
14574 "resolve winning blob row audience for {:?}/{:?}: {error}",
14575 binding.table(),
14576 binding.row_id()
14577 ))
14578 },
14579 )?;
14580 let live_audience = RemoteAudience::try_from(live_audience).map_err(|error| {
14581 DbError::Message(format!(
14582 "winning blob row {:?}/{:?} is not remote: {error}",
14583 binding.table(),
14584 binding.row_id()
14585 ))
14586 })?;
14587 if live_audience != package_audience {
14588 return Err(DbError::Message(format!(
14589 "winning blob row {:?}/{:?} belongs to {:?}, but its package belongs to {:?}",
14590 binding.table(),
14591 binding.row_id(),
14592 live_audience,
14593 package_audience
14594 )));
14595 }
14596 validate_live_blob_row(binding, declaration, &row, &live_audience)?;
14597
14598 let locator = binding.blob().locator();
14599 let locator_hash = locator.locator_hash();
14600 let remote_object_id = remote_object_id(binding.blob().object());
14601 let remote = load_remote_object_on(conn, remote_object_id)?;
14602 if !remote.is_activated_stored_blob() {
14603 return Err(DbError::Message(format!(
14604 "blob locator {locator_hash} does not reference an activated uploaded blob"
14605 )));
14606 }
14607 validate_remote_object_on(
14608 conn,
14609 remote_object_id,
14610 binding.blob().object(),
14611 &locator.to_bytes(),
14612 RemoteStoredRepresentationRef::Blob,
14613 )?;
14614 conn.execute(
14615 "INSERT INTO blob_locators
14616 (remote_object_id, locator_hash)
14617 VALUES (?1, ?2)
14618 ON CONFLICT(remote_object_id) DO NOTHING",
14619 rusqlite::params![remote_object_id.to_string(), locator_hash.to_string(),],
14620 )
14621 .map_err(DbError::from)?;
14622 validate_stored_locator_on(conn, binding.blob())?;
14623
14624 let audience_authority =
14625 serde_json::to_string(package.audience()).map_err(|error| {
14626 DbError::Message(format!("serialize row blob audience authority: {error}"))
14627 })?;
14628 conn.execute(
14629 "INSERT INTO row_blob_locators
14630 (table_name, row_id, column_name, row_stamp, audience_authority, remote_object_id)
14631 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
14632 ON CONFLICT(table_name, row_id, column_name, row_stamp) DO NOTHING",
14633 rusqlite::params![
14634 binding.table(),
14635 binding.row_id(),
14636 binding.column(),
14637 binding.row_stamp(),
14638 audience_authority,
14639 remote_object_id.to_string(),
14640 ],
14641 )
14642 .map_err(DbError::from)?;
14643 validate_stored_row_binding_on(conn, binding, package.audience(), remote_object_id)?;
14644 installed += 1;
14645 }
14646 Ok(installed)
14647 }
14648
14649 pub(crate) fn install_pulled_blob_activations_on(
14650 conn: &Connection,
14651 package: &AudiencePackage,
14652 owner: &StoreBatchCommitRef,
14653 ) -> Result<(), DbError> {
14654 if package.commit_coord() != &owner.coord {
14655 return Err(DbError::Message(
14656 "pulled blob package coordinate differs from its activating commit".to_string(),
14657 ));
14658 }
14659 for binding in package.blob_bindings() {
14660 let stored = binding.blob();
14661 let object_id = remote_object_id(stored.object());
14662 let exists: bool = conn
14663 .query_row(
14664 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
14665 [object_id.to_string()],
14666 |row| row.get(0),
14667 )
14668 .map_err(DbError::from)?;
14669 let remote = if exists {
14670 let mut remote = load_remote_object_on(conn, object_id)?;
14671 remote
14672 .merge_blob_activation(stored, owner)
14673 .map_err(|error| {
14674 DbError::Message(format!(
14675 "merge pulled blob activation {object_id}: {error}"
14676 ))
14677 })?;
14678 remote
14679 } else {
14680 RemoteObjectRecord::activated_blob(stored, owner.clone()).map_err(|error| {
14681 DbError::Message(format!(
14682 "construct pulled blob activation {object_id}: {error}"
14683 ))
14684 })?
14685 };
14686 let state = serde_json::to_string(&remote).map_err(|error| {
14687 DbError::Message(format!("serialize pulled blob activation: {error}"))
14688 })?;
14689 conn.execute(
14690 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2) \
14691 ON CONFLICT(object_id) DO UPDATE SET state = excluded.state",
14692 rusqlite::params![object_id.to_string(), state],
14693 )
14694 .map_err(DbError::from)?;
14695 }
14696 Ok(())
14697 }
14698
14699 pub async fn row_blob_ref(&self, table: &str, row_id: &str) -> Result<RowBlobRef, DbError> {
14700 let table = self
14701 .synced_tables()
14702 .iter()
14703 .find(|candidate| candidate.name() == table)
14704 .cloned()
14705 .ok_or_else(|| DbError::Message(format!("undeclared synced table {table:?}")))?;
14706 if table.blob().is_none() {
14707 return Err(DbError::Message(format!(
14708 "synced table {:?} has no blob declaration",
14709 table.name()
14710 )));
14711 }
14712 let row_id = row_id.to_string();
14713 let gates = self.gates();
14714 self.call(move |conn| Self::row_blob_ref_on(conn, &gates, &table, &row_id))
14715 .await
14716 }
14717
14718 pub async fn validate_row_blob_ref(&self, reference: &RowBlobRef) -> Result<(), DbError> {
14719 let current = self
14720 .row_blob_ref(reference.table(), reference.row_id())
14721 .await?;
14722 if ¤t != reference {
14723 return Err(DbError::Message(format!(
14724 "row blob reference {:?}/{:?}/{:?} at {:?} is stale",
14725 reference.table(),
14726 reference.row_id(),
14727 reference.column(),
14728 reference.row_stamp()
14729 )));
14730 }
14731 Ok(())
14732 }
14733
14734 pub async fn row_blob_refs_for_root(
14735 &self,
14736 root_table: &str,
14737 root_id: &str,
14738 ) -> Result<Vec<RowBlobRef>, DbError> {
14739 let root_table = root_table.to_string();
14740 let root_id = root_id.to_string();
14741 let gates = self.gates();
14742 let tables = self.synced_tables().to_vec();
14743 self.call(move |conn| {
14744 Self::row_blob_refs_for_root_on(conn, &gates, &tables, &root_table, &root_id)
14745 })
14746 .await
14747 }
14748
14749 pub(crate) fn row_blob_refs_for_root_on(
14750 conn: &Connection,
14751 gates: &Gates,
14752 tables: &[SyncedTable],
14753 root_table: &str,
14754 root_id: &str,
14755 ) -> Result<Vec<RowBlobRef>, DbError> {
14756 let mut rows = gates
14757 .subtree_rows(conn, root_table, root_id)
14758 .map_err(|error| DbError::Message(error.to_string()))?
14759 .into_iter()
14760 .collect::<Vec<_>>();
14761 rows.sort();
14762 let tables = tables
14763 .iter()
14764 .map(|table| (table.name(), table))
14765 .collect::<BTreeMap<_, _>>();
14766 rows.into_iter()
14767 .filter_map(|(table_name, row_id)| {
14768 tables
14769 .get(table_name.as_str())
14770 .filter(|table| table.blob().is_some())
14771 .map(|table| Self::row_blob_ref_on(conn, gates, table, &row_id))
14772 })
14773 .collect()
14774 }
14775
14776 pub(crate) fn upload_entries_for_rows_on(
14777 conn: &Connection,
14778 rows: &[RowBlobRef],
14779 ) -> Result<Vec<OutboxEntry>, DbError> {
14780 rows.iter()
14781 .filter_map(|row| {
14782 match upload_entry_for_identity_on(
14783 conn,
14784 row.table(),
14785 row.row_id(),
14786 row.column(),
14787 row.row_stamp(),
14788 ) {
14789 Ok(Some(entry)) => Some(Ok(entry)),
14790 Ok(None) => None,
14791 Err(error) => Some(Err(error)),
14792 }
14793 })
14794 .collect()
14795 }
14796
14797 pub(crate) fn upload_entries_for_root_on(
14798 conn: &Connection,
14799 gates: &Gates,
14800 tables: &[SyncedTable],
14801 root_table: &str,
14802 root_id: &str,
14803 ) -> Result<Vec<OutboxEntry>, DbError> {
14804 let rows = Self::row_blob_refs_for_root_on(conn, gates, tables, root_table, root_id)?;
14805 Self::upload_entries_for_rows_on(conn, &rows)
14806 }
14807
14808 pub(crate) fn stored_blob_reference_state_on(
14809 conn: &Connection,
14810 gates: &Gates,
14811 tables: &[SyncedTable],
14812 stored: &StoredBlobRef,
14813 ) -> Result<StoredBlobReferenceState, DbError> {
14814 let exact_object_id = remote_object_id(stored.object()).to_string();
14815 let mut statement = conn
14816 .prepare(
14817 "SELECT table_name, row_id, row_stamp FROM row_blob_locators
14818 WHERE remote_object_id = ?1",
14819 )
14820 .map_err(DbError::from)?;
14821 let bindings = statement
14822 .query_map([exact_object_id], |row| {
14823 Ok((
14824 row.get::<_, String>(0)?,
14825 row.get::<_, String>(1)?,
14826 row.get::<_, String>(2)?,
14827 ))
14828 })
14829 .map_err(DbError::from)?
14830 .collect::<Result<Vec<_>, _>>()
14831 .map_err(DbError::from)?;
14832 drop(statement);
14833 let mut unresolved = false;
14834 for (table_name, row_id, row_stamp) in bindings {
14835 let table = tables
14836 .iter()
14837 .find(|candidate| candidate.name() == table_name)
14838 .ok_or_else(|| {
14839 DbError::Message(format!(
14840 "stored blob binding names undeclared table {table_name:?}"
14841 ))
14842 })?;
14843 let declaration = table.blob().ok_or_else(|| {
14844 DbError::Message(format!(
14845 "stored blob binding names table {table_name:?} without a blob declaration"
14846 ))
14847 })?;
14848 let Some(live) = live_blob_row(conn, &table_name, &row_id, declaration)? else {
14849 continue;
14850 };
14851 if live.stamp != row_stamp {
14852 continue;
14853 }
14854 match gates
14855 .root_kept_of(conn, &table_name, &row_id)
14856 .map_err(|error| DbError::Message(error.to_string()))?
14857 {
14858 Some(false) => continue,
14859 None => {
14860 unresolved = true;
14861 continue;
14862 }
14863 Some(true) => {}
14864 }
14865 let reference = Self::row_blob_ref_on(conn, gates, table, &row_id)?;
14866 if matches!(reference.authority(), RowBlobAuthority::Remote(_))
14867 && reference.stored() == Some(stored)
14868 {
14869 return Ok(StoredBlobReferenceState::LiveRemote);
14870 }
14871 }
14872 Ok(if unresolved {
14873 StoredBlobReferenceState::Unresolved
14874 } else {
14875 StoredBlobReferenceState::NotLiveRemote
14876 })
14877 }
14878
14879 pub(crate) fn row_blob_ref_on(
14880 conn: &Connection,
14881 gates: &Gates,
14882 table: &SyncedTable,
14883 row_id: &str,
14884 ) -> Result<RowBlobRef, DbError> {
14885 let declaration = table.blob().ok_or_else(|| {
14886 DbError::Message(format!(
14887 "synced table {:?} has no blob declaration",
14888 table.name()
14889 ))
14890 })?;
14891 let row = live_blob_row(conn, table.name(), row_id, declaration)?.ok_or_else(|| {
14892 DbError::Message(format!(
14893 "blob-bearing row {:?}/{row_id:?} does not exist",
14894 table.name()
14895 ))
14896 })?;
14897 let audience =
14898 gate::live_row_audience(conn, gates, table.name(), row_id).map_err(|error| {
14899 DbError::Message(format!(
14900 "resolve blob row audience for {:?}/{row_id:?}: {error}",
14901 table.name()
14902 ))
14903 })?;
14904 let (authority, stored) = match RemoteAudience::try_from(audience.clone()) {
14905 Err(_) if audience == Audience::Local => (RowBlobAuthority::Local, None),
14906 Err(error) => {
14907 return Err(DbError::Message(format!(
14908 "blob row {:?}/{row_id:?} has invalid audience: {error}",
14909 table.name()
14910 )));
14911 }
14912 Ok(remote_audience) => {
14913 let installed: Option<(String, String)> = conn
14914 .query_row(
14915 "SELECT binding.audience_authority, locator.remote_object_id
14916 FROM row_blob_locators AS binding
14917 JOIN blob_locators AS locator
14918 ON locator.remote_object_id = binding.remote_object_id
14919 WHERE binding.table_name = ?1
14920 AND binding.row_id = ?2
14921 AND binding.column_name = ?3
14922 AND binding.row_stamp = ?4",
14923 rusqlite::params![table.name(), row_id, declaration.id_column, row.stamp,],
14924 |row| Ok((row.get(0)?, row.get(1)?)),
14925 )
14926 .optional()
14927 .map_err(DbError::from)?;
14928 let exact = if let Some((authority_json, remote_object_id)) = installed {
14929 let package_authority: crate::sync::audience_package::PackageAudience =
14930 serde_json::from_str(&authority_json).map_err(|error| {
14931 DbError::Message(format!(
14932 "remote blob row {:?}/{row_id:?} has invalid audience authority: {error}",
14933 table.name()
14934 ))
14935 })?;
14936 let remote_object_id = remote_object_id.parse().map_err(|error| {
14937 DbError::Message(format!(
14938 "remote blob row {:?}/{row_id:?} has invalid prepared object id: {error}",
14939 table.name()
14940 ))
14941 })?;
14942 let remote = load_remote_object_on(conn, remote_object_id)?;
14943 if !remote.is_activated_stored_blob() {
14944 return Err(DbError::Message(format!(
14945 "remote blob row {:?}/{row_id:?} references a blob without activated ownership",
14946 table.name()
14947 )));
14948 }
14949 let locator = BlobLocator::parse(remote.bytes().canonical_semantic_bytes())
14950 .map_err(|error| {
14951 DbError::Message(format!(
14952 "remote blob row {:?}/{row_id:?} has invalid locator: {error}",
14953 table.name()
14954 ))
14955 })?;
14956 let stored = StoredBlobRef::new(locator, remote.object().clone()).map_err(
14957 |error| {
14958 DbError::Message(format!(
14959 "remote blob row {:?}/{row_id:?} has invalid stored blob reference: {error}",
14960 table.name()
14961 ))
14962 },
14963 )?;
14964 Some((package_authority, stored))
14965 } else {
14966 created_upload_handoff_on(
14967 conn,
14968 table.name(),
14969 row_id,
14970 &declaration.id_column,
14971 &row.stamp,
14972 )?
14973 .map(|handoff| (handoff.authority, handoff.stored))
14974 };
14975 let Some((package_authority, stored)) = exact else {
14976 return RowBlobRef::new(
14977 table.name().to_string(),
14978 row_id.to_string(),
14979 row.stamp,
14980 declaration.id_column.clone(),
14981 BlobRef {
14982 namespace: declaration.namespace.clone(),
14983 id: row.blob_id,
14984 scope: declaration.scope.clone(),
14985 cloud_path: row.cloud_path,
14986 provenance: declaration.provenance,
14987 fill: declaration.fill,
14988 },
14989 row.plaintext_size,
14990 row.plaintext_hash,
14991 RowBlobAuthority::PendingRemote(remote_audience),
14992 None,
14993 )
14994 .map_err(DbError::Message);
14995 };
14996 if package_authority.remote_audience() != remote_audience {
14997 return Err(DbError::Message(format!(
14998 "remote blob row {:?}/{row_id:?} has audience authority {:?}, expected {remote_audience:?}",
14999 table.name(), package_authority
15000 )));
15001 }
15002 validate_live_blob_locator(
15003 table.name(),
15004 row_id,
15005 &declaration.id_column,
15006 &row.stamp,
15007 &stored,
15008 declaration,
15009 &row,
15010 &remote_audience,
15011 )?;
15012 (RowBlobAuthority::Remote(package_authority), Some(stored))
15013 }
15014 };
15015 let blob = BlobRef {
15016 namespace: declaration.namespace.clone(),
15017 id: row.blob_id.clone(),
15018 scope: declaration.scope.clone(),
15019 cloud_path: row.cloud_path.clone(),
15020 provenance: declaration.provenance,
15021 fill: declaration.fill,
15022 };
15023 RowBlobRef::new(
15024 table.name().to_string(),
15025 row_id.to_string(),
15026 row.stamp,
15027 declaration.id_column.clone(),
15028 blob,
15029 row.plaintext_size,
15030 row.plaintext_hash,
15031 authority,
15032 stored,
15033 )
15034 .map_err(DbError::Message)
15035 }
15036
15037 pub fn enqueue_upload_on(
15040 conn: &Connection,
15041 root_table: &str,
15042 root_id: &str,
15043 row: &RowBlobRef,
15044 source_path: &Path,
15045 retain_pinned: bool,
15046 created_at: &str,
15047 ) -> Result<(), DbError> {
15048 if row.authority() != &RowBlobAuthority::Local || row.stored().is_some() {
15049 return Err(DbError::Message(
15050 "cloud upload requires an exact Local row blob reference".to_string(),
15051 ));
15052 }
15053 let source_path = source_path.to_str().ok_or_else(|| {
15054 DbError::Message(format!(
15055 "blob source path for {}/{}/{} is not UTF-8: {source_path:?}",
15056 row.table(),
15057 row.row_id(),
15058 row.column()
15059 ))
15060 })?;
15061 let encoded = serde_json::to_string(row)
15062 .map_err(|error| DbError::Message(format!("serialize row blob ref: {error}")))?;
15063 let pending = serde_json::to_string(&OutboxUploadState::Pending).map_err(|error| {
15064 DbError::Message(format!("serialize pending blob upload state: {error}"))
15065 })?;
15066 conn.execute(
15067 "INSERT INTO cloud_outbox
15068 (operation, table_name, row_id, column_name, row_stamp, root_table, root_id,
15069 row_ref, upload_state, source_path, retain_pinned, created_at)
15070 VALUES ('upload', ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
15071 ON CONFLICT(operation, table_name, row_id, column_name, row_stamp) DO UPDATE SET
15072 root_table = excluded.root_table,
15073 root_id = excluded.root_id,
15074 source_path = excluded.source_path,
15075 retain_pinned = excluded.retain_pinned,
15076 attempt_count = 0,
15077 last_error = NULL,
15078 last_attempt_at = NULL
15079 WHERE cloud_outbox.row_ref = excluded.row_ref
15080 AND cloud_outbox.root_table = excluded.root_table
15081 AND cloud_outbox.root_id = excluded.root_id",
15082 rusqlite::params![
15083 row.table(),
15084 row.row_id(),
15085 row.column(),
15086 row.row_stamp(),
15087 root_table,
15088 root_id,
15089 encoded,
15090 pending,
15091 source_path,
15092 retain_pinned,
15093 created_at,
15094 ],
15095 )
15096 .map_err(DbError::from)
15097 .and_then(|changed| {
15098 if changed == 1 {
15099 Ok(())
15100 } else {
15101 Err(DbError::Message(format!(
15102 "upload outbox identity {}/{}/{}/{} carries different row facts",
15103 row.table(),
15104 row.row_id(),
15105 row.column(),
15106 row.row_stamp()
15107 )))
15108 }
15109 })
15110 }
15111
15112 pub async fn mark_cloud_upload_prepared(
15113 &self,
15114 entry: &OutboxEntry,
15115 authority: crate::sync::audience_package::PackageAudience,
15116 stored: StoredBlobRef,
15117 spool_path: PathBuf,
15118 ) -> Result<(), DbError> {
15119 let OutboxOperation::Upload { row, state, .. } = &entry.operation else {
15120 return Err(DbError::Message(
15121 "only an upload outbox entry can own a prepared blob".to_string(),
15122 ));
15123 };
15124 if state != &OutboxUploadState::Pending {
15125 return Err(DbError::Message(
15126 "blob upload is already prepared".to_string(),
15127 ));
15128 }
15129 let locator = stored.locator();
15130 if locator.namespace() != row.blob().namespace
15131 || locator.blob_id() != row.blob().id
15132 || locator.plaintext_size() != row.plaintext_size()
15133 || locator.plaintext_hash() != row.plaintext_hash()
15134 || locator
15135 .scope()
15136 .is_some_and(|scope| scope != &row.blob().scope)
15137 {
15138 return Err(DbError::Message(
15139 "prepared blob differs from its exact Local row version".to_string(),
15140 ));
15141 }
15142 if locator.audience() != authority.remote_audience() {
15143 return Err(DbError::Message(
15144 "prepared blob audience differs from its package authority".to_string(),
15145 ));
15146 }
15147 let prepared = OutboxUploadState::Prepared {
15148 authority,
15149 stored,
15150 spool_path,
15151 };
15152 let prepared_json = serde_json::to_string(&prepared).map_err(|error| {
15153 DbError::Message(format!("serialize prepared blob upload: {error}"))
15154 })?;
15155 let pending_json = serde_json::to_string(&OutboxUploadState::Pending)
15156 .map_err(|error| DbError::Message(format!("serialize pending blob upload: {error}")))?;
15157 let id = entry.id;
15158 let table = row.table().to_string();
15159 let row_id = row.row_id().to_string();
15160 let column = row.column().to_string();
15161 let row_stamp = row.row_stamp().to_string();
15162 self.call(move |conn| {
15163 let updated = conn
15164 .execute(
15165 "UPDATE cloud_outbox SET upload_state = ?1
15166 WHERE id = ?2 AND operation = 'upload' AND table_name = ?3
15167 AND row_id = ?4 AND column_name = ?5 AND row_stamp = ?6
15168 AND upload_state = ?7",
15169 rusqlite::params![
15170 prepared_json,
15171 id,
15172 table,
15173 row_id,
15174 column,
15175 row_stamp,
15176 pending_json,
15177 ],
15178 )
15179 .map_err(DbError::from)?;
15180 if updated != 1 {
15181 return Err(DbError::Message(
15182 "upload outbox entry changed before prepared-object handoff".to_string(),
15183 ));
15184 }
15185 Ok(())
15186 })
15187 .await
15188 }
15189
15190 pub async fn mark_cloud_upload_created(&self, entry: &OutboxEntry) -> Result<(), DbError> {
15191 let OutboxOperation::Upload { row, state, .. } = &entry.operation else {
15192 return Err(DbError::Message(
15193 "only a prepared upload outbox entry can record cloud creation".to_string(),
15194 ));
15195 };
15196 let OutboxUploadState::Prepared {
15197 authority,
15198 stored,
15199 spool_path,
15200 } = state
15201 else {
15202 return Err(DbError::Message(
15203 "cloud creation requires a prepared upload object".to_string(),
15204 ));
15205 };
15206 let created_json = serde_json::to_string(&OutboxUploadState::Created {
15207 authority: authority.clone(),
15208 stored: stored.clone(),
15209 spool_path: spool_path.clone(),
15210 })
15211 .map_err(|error| DbError::Message(format!("serialize created blob upload: {error}")))?;
15212 let prepared_json = serde_json::to_string(state).map_err(|error| {
15213 DbError::Message(format!("serialize prepared blob upload identity: {error}"))
15214 })?;
15215 let id = entry.id;
15216 let table = row.table().to_string();
15217 let row_id = row.row_id().to_string();
15218 let column = row.column().to_string();
15219 let row_stamp = row.row_stamp().to_string();
15220 self.call(move |conn| {
15221 let updated = conn
15222 .execute(
15223 "UPDATE cloud_outbox SET upload_state = ?1
15224 WHERE id = ?2 AND operation = 'upload' AND table_name = ?3
15225 AND row_id = ?4 AND column_name = ?5 AND row_stamp = ?6
15226 AND upload_state = ?7",
15227 rusqlite::params![
15228 created_json,
15229 id,
15230 table,
15231 row_id,
15232 column,
15233 row_stamp,
15234 prepared_json,
15235 ],
15236 )
15237 .map_err(DbError::from)?;
15238 if updated != 1 {
15239 return Err(DbError::Message(
15240 "upload outbox entry changed before cloud-created handoff".to_string(),
15241 ));
15242 }
15243 Ok(())
15244 })
15245 .await
15246 }
15247
15248 pub fn enqueue_delete_on(
15249 conn: &Connection,
15250 stored: &StoredBlobRef,
15251 created_at: &str,
15252 ) -> Result<(), DbError> {
15253 let encoded = serde_json::to_string(stored)
15254 .map_err(|error| DbError::Message(format!("serialize stored blob ref: {error}")))?;
15255 conn.execute(
15256 "INSERT INTO cloud_outbox (operation, stored_ref, created_at)
15257 VALUES ('delete', ?1, ?2)
15258 ON CONFLICT(stored_ref) DO UPDATE SET
15259 created_at = excluded.created_at,
15260 attempt_count = 0,
15261 last_error = NULL,
15262 last_attempt_at = NULL",
15263 (encoded, created_at),
15264 )
15265 .map(|_| ())
15266 .map_err(DbError::from)
15267 }
15268
15269 pub async fn get_pending_cloud_uploads(&self) -> Result<Vec<OutboxEntry>, DbError> {
15270 self.pending_outbox("upload").await
15271 }
15272
15273 pub async fn get_pending_cloud_deletes(&self) -> Result<Vec<OutboxEntry>, DbError> {
15274 self.pending_outbox("delete").await
15275 }
15276
15277 async fn pending_outbox(&self, operation: &'static str) -> Result<Vec<OutboxEntry>, DbError> {
15278 self.call(move |conn| {
15279 let mut statement = conn
15280 .prepare(
15281 "SELECT id, operation, row_ref, stored_ref, source_path, retain_pinned,
15282 upload_state, attempt_count, last_attempt_at, root_table, root_id
15283 FROM cloud_outbox WHERE operation = ?1 ORDER BY id",
15284 )
15285 .map_err(DbError::from)?;
15286 let entries = statement
15287 .query_map([operation], row_to_outbox_entry)
15288 .map_err(DbError::from)?
15289 .collect::<Result<Vec<_>, _>>()
15290 .map_err(DbError::from)?;
15291 Ok(entries)
15292 })
15293 .await
15294 }
15295
15296 pub async fn remove_cloud_outbox_entry(&self, entry: &OutboxEntry) -> Result<(), DbError> {
15297 let entry = entry.clone();
15298 self.call(move |conn| Self::remove_cloud_outbox_entry_on(conn, &entry))
15299 .await
15300 }
15301
15302 pub(crate) fn remove_cloud_outbox_entry_on(
15303 conn: &Connection,
15304 entry: &OutboxEntry,
15305 ) -> Result<(), DbError> {
15306 let identity = outbox_identity(&entry.operation)?;
15307 let removed = match identity {
15308 OutboxIdentity::Upload {
15309 table,
15310 row_id,
15311 column,
15312 row_stamp,
15313 } => conn.execute(
15314 "DELETE FROM cloud_outbox WHERE id = ?1 AND operation = 'upload'
15315 AND table_name = ?2 AND row_id = ?3 AND column_name = ?4 AND row_stamp = ?5",
15316 rusqlite::params![entry.id, table, row_id, column, row_stamp],
15317 ),
15318 OutboxIdentity::Stored { operation, stored } => conn.execute(
15319 "DELETE FROM cloud_outbox WHERE id = ?1 AND operation = ?2 AND stored_ref = ?3",
15320 rusqlite::params![entry.id, operation, stored],
15321 ),
15322 }
15323 .map_err(DbError::from)?;
15324 if removed != 1 {
15325 return Err(DbError::Message(
15326 "cloud outbox entry changed before exact dequeue".to_string(),
15327 ));
15328 }
15329 Ok(())
15330 }
15331
15332 pub(crate) fn finish_cancelled_upload_on(
15333 conn: &Connection,
15334 entry: &OutboxEntry,
15335 ) -> Result<bool, DbError> {
15336 let OutboxOperation::Upload {
15337 root_table,
15338 root_id,
15339 ..
15340 } = &entry.operation
15341 else {
15342 return Err(DbError::Message(
15343 "make_remote cleanup requires an upload entry".to_string(),
15344 ));
15345 };
15346 if !matches!(
15347 Self::make_remote_intent_state(conn, root_table, root_id)?,
15348 Some(MakeRemoteIntentState::Cancelling)
15349 ) {
15350 return Err(DbError::Message(format!(
15351 "make_remote cleanup for {root_table:?}/{root_id:?} lost cancellation ownership"
15352 )));
15353 }
15354 Self::remove_cloud_outbox_entry_on(conn, entry)?;
15355 let remaining: i64 = conn
15356 .query_row(
15357 "SELECT COUNT(*) FROM cloud_outbox
15358 WHERE operation = 'upload' AND root_table = ?1 AND root_id = ?2",
15359 (root_table, root_id),
15360 |row| row.get(0),
15361 )
15362 .map_err(DbError::from)?;
15363 if remaining != 0 {
15364 return Ok(false);
15365 }
15366 let removed = conn
15367 .execute(
15368 "DELETE FROM blob_make_remote_intents
15369 WHERE root_table = ?1 AND root_id = ?2 AND state = 'cancelling'",
15370 (root_table, root_id),
15371 )
15372 .map_err(DbError::from)?;
15373 if removed != 1 {
15374 return Err(DbError::Message(format!(
15375 "make_remote cancellation {root_table:?}/{root_id:?} changed before completion"
15376 )));
15377 }
15378 Ok(true)
15379 }
15380
15381 pub async fn record_cloud_outbox_failure(
15382 &self,
15383 entry: &OutboxEntry,
15384 error: &str,
15385 attempted_at: &str,
15386 ) -> Result<(), DbError> {
15387 let id = entry.id;
15388 let identity = outbox_identity(&entry.operation)?;
15389 let error = error.to_string();
15390 let attempted_at = attempted_at.to_string();
15391 self.call(move |conn| {
15392 let updated = match identity {
15393 OutboxIdentity::Upload {
15394 table,
15395 row_id,
15396 column,
15397 row_stamp,
15398 } => conn.execute(
15399 "UPDATE cloud_outbox SET attempt_count = attempt_count + 1,
15400 last_error = ?1, last_attempt_at = ?2
15401 WHERE id = ?3 AND operation = 'upload' AND table_name = ?4
15402 AND row_id = ?5 AND column_name = ?6 AND row_stamp = ?7",
15403 rusqlite::params![error, attempted_at, id, table, row_id, column, row_stamp],
15404 ),
15405 OutboxIdentity::Stored { operation, stored } => conn.execute(
15406 "UPDATE cloud_outbox SET attempt_count = attempt_count + 1,
15407 last_error = ?1, last_attempt_at = ?2
15408 WHERE id = ?3 AND operation = ?4 AND stored_ref = ?5",
15409 rusqlite::params![error, attempted_at, id, operation, stored],
15410 ),
15411 }
15412 .map_err(DbError::from)?;
15413 if updated != 1 {
15414 return Err(DbError::Message(
15415 "cloud outbox entry changed before failure recording".to_string(),
15416 ));
15417 }
15418 Ok(())
15419 })
15420 .await
15421 }
15422
15423 pub async fn reset_cloud_outbox_backoff(&self) -> Result<(), DbError> {
15424 self.call(|conn| {
15425 conn.execute(
15426 "UPDATE cloud_outbox SET last_attempt_at = NULL WHERE attempt_count > 0",
15427 [],
15428 )
15429 .map(|_| ())
15430 .map_err(DbError::from)
15431 })
15432 .await
15433 }
15434
15435 pub fn register_external_blob_on(
15438 conn: &Connection,
15439 reference: &RowBlobRef,
15440 path: &Path,
15441 ) -> Result<(), DbError> {
15442 if reference.authority() != &RowBlobAuthority::Local || reference.stored().is_some() {
15443 return Err(DbError::Message(
15444 "external file requires an exact Local row blob reference".to_string(),
15445 ));
15446 }
15447 let path = path.to_str().ok_or_else(|| {
15448 DbError::Message(format!("external blob path is not UTF-8: {path:?}"))
15449 })?;
15450 let size = i64::try_from(reference.plaintext_size()).map_err(|_| {
15451 DbError::Message("external blob plaintext size exceeds SQLite INTEGER".to_string())
15452 })?;
15453 conn.execute(
15454 "INSERT INTO local_blob_refs
15455 (table_name, row_id, column_name, row_stamp, namespace, blob_id,
15456 path, plaintext_size, plaintext_hash)
15457 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
15458 ON CONFLICT(table_name, row_id, column_name, row_stamp) DO UPDATE SET
15459 namespace = excluded.namespace,
15460 blob_id = excluded.blob_id,
15461 path = excluded.path,
15462 plaintext_size = excluded.plaintext_size,
15463 plaintext_hash = excluded.plaintext_hash",
15464 rusqlite::params![
15465 reference.table(),
15466 reference.row_id(),
15467 reference.column(),
15468 reference.row_stamp(),
15469 &reference.blob().namespace,
15470 &reference.blob().id,
15471 path,
15472 size,
15473 reference.plaintext_hash().to_string(),
15474 ],
15475 )
15476 .map(|_| ())
15477 .map_err(DbError::from)
15478 }
15479
15480 pub fn clear_external_blob_on(
15481 conn: &Connection,
15482 reference: &RowBlobRef,
15483 ) -> Result<(), DbError> {
15484 conn.execute(
15485 "DELETE FROM local_blob_refs WHERE table_name = ?1 AND row_id = ?2
15486 AND column_name = ?3 AND row_stamp = ?4",
15487 rusqlite::params![
15488 reference.table(),
15489 reference.row_id(),
15490 reference.column(),
15491 reference.row_stamp(),
15492 ],
15493 )
15494 .map(|_| ())
15495 .map_err(DbError::from)
15496 }
15497
15498 pub fn insert_make_remote_intent_on(
15499 conn: &Connection,
15500 root_table: &str,
15501 root_id: &str,
15502 retain_pinned: bool,
15503 ) -> Result<(), DbError> {
15504 conn.execute(
15505 "INSERT INTO blob_make_remote_intents
15506 (root_table, root_id, retain_pinned, state, write_id)
15507 VALUES (?1, ?2, ?3, 'uploading', NULL)
15508 ON CONFLICT(root_table, root_id) DO UPDATE SET
15509 retain_pinned = excluded.retain_pinned
15510 WHERE blob_make_remote_intents.state = 'uploading'",
15511 (root_table, root_id, retain_pinned),
15512 )
15513 .map_err(DbError::from)
15514 .and_then(|changed| {
15515 if changed == 1 {
15516 Ok(())
15517 } else {
15518 Err(DbError::Message(format!(
15519 "make_remote for {root_table:?}/{root_id:?} is already publishing"
15520 )))
15521 }
15522 })
15523 }
15524
15525 #[cfg(test)]
15526 pub(crate) fn make_remote_intent_exists(
15527 conn: &Connection,
15528 root_table: &str,
15529 root_id: &str,
15530 ) -> Result<bool, DbError> {
15531 conn.query_row(
15532 "SELECT 1 FROM blob_make_remote_intents WHERE root_table = ?1 AND root_id = ?2",
15533 (root_table, root_id),
15534 |_| Ok(()),
15535 )
15536 .optional()
15537 .map(|value| value.is_some())
15538 .map_err(DbError::from)
15539 }
15540
15541 pub(crate) fn make_remote_intent_state(
15542 conn: &Connection,
15543 root_table: &str,
15544 root_id: &str,
15545 ) -> Result<Option<MakeRemoteIntentState>, DbError> {
15546 let encoded = conn
15547 .query_row(
15548 "SELECT state, write_id FROM blob_make_remote_intents
15549 WHERE root_table = ?1 AND root_id = ?2",
15550 (root_table, root_id),
15551 |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?)),
15552 )
15553 .optional()
15554 .map_err(DbError::from)?;
15555 encoded
15556 .map(|(state, write_id)| match (state.as_str(), write_id) {
15557 ("uploading", None) => Ok(MakeRemoteIntentState::Uploading),
15558 ("cancelling", None) => Ok(MakeRemoteIntentState::Cancelling),
15559 ("publishing", Some(write_id)) => Ok(MakeRemoteIntentState::Publishing(
15560 WriteId::from_generated(write_id),
15561 )),
15562 _ => Err(DbError::Message(format!(
15563 "make_remote intent {root_table:?}/{root_id:?} has invalid state {state:?}"
15564 ))),
15565 })
15566 .transpose()
15567 }
15568
15569 pub(crate) fn mark_make_remote_cancelling_on(
15570 conn: &Connection,
15571 root_table: &str,
15572 root_id: &str,
15573 ) -> Result<(), DbError> {
15574 let updated = conn
15575 .execute(
15576 "UPDATE blob_make_remote_intents SET state = 'cancelling'
15577 WHERE root_table = ?1 AND root_id = ?2 AND state = 'uploading'",
15578 (root_table, root_id),
15579 )
15580 .map_err(DbError::from)?;
15581 if updated == 1 {
15582 conn.execute(
15583 "UPDATE cloud_outbox
15584 SET attempt_count = 0, last_error = NULL, last_attempt_at = NULL
15585 WHERE operation = 'upload' AND root_table = ?1 AND root_id = ?2",
15586 (root_table, root_id),
15587 )
15588 .map_err(DbError::from)?;
15589 return Ok(());
15590 }
15591 if matches!(
15592 Self::make_remote_intent_state(conn, root_table, root_id)?,
15593 Some(MakeRemoteIntentState::Cancelling)
15594 ) {
15595 return Ok(());
15596 }
15597 Err(DbError::Message(format!(
15598 "make_remote intent {root_table:?}/{root_id:?} cannot enter cancellation"
15599 )))
15600 }
15601
15602 pub fn mark_make_remote_publishing_on(
15603 conn: &Connection,
15604 root_table: &str,
15605 root_id: &str,
15606 write_id: &WriteId,
15607 ) -> Result<(), DbError> {
15608 let updated = conn
15609 .execute(
15610 "UPDATE blob_make_remote_intents SET state = 'publishing', write_id = ?3
15611 WHERE root_table = ?1 AND root_id = ?2 AND state = 'uploading'",
15612 (root_table, root_id, write_id.as_str()),
15613 )
15614 .map_err(DbError::from)?;
15615 if updated != 1 {
15616 return Err(DbError::Message(format!(
15617 "make_remote intent {root_table:?}/{root_id:?} changed before publication"
15618 )));
15619 }
15620 Ok(())
15621 }
15622
15623 pub fn complete_make_remote_publication_on(
15624 conn: &Connection,
15625 write_id: &WriteId,
15626 ) -> Result<(), DbError> {
15627 let removed = conn
15628 .execute(
15629 "DELETE FROM blob_make_remote_intents
15630 WHERE write_id = ?1 AND state = 'publishing'",
15631 [write_id.as_str()],
15632 )
15633 .map_err(DbError::from)?;
15634 if removed != 1 {
15635 return Err(DbError::Message(format!(
15636 "make_remote publication {write_id} changed before activation"
15637 )));
15638 }
15639 Ok(())
15640 }
15641
15642 pub(crate) fn make_remote_publication_root_on(
15643 conn: &Connection,
15644 write_id: &WriteId,
15645 ) -> Result<Option<(String, String)>, DbError> {
15646 conn.query_row(
15647 "SELECT root_table, root_id FROM blob_make_remote_intents
15648 WHERE write_id = ?1 AND state = 'publishing'",
15649 [write_id.as_str()],
15650 |row| Ok((row.get(0)?, row.get(1)?)),
15651 )
15652 .optional()
15653 .map_err(DbError::from)
15654 }
15655
15656 pub fn make_remote_intent_retain_pinned(
15657 conn: &Connection,
15658 root_table: &str,
15659 root_id: &str,
15660 ) -> Result<Option<bool>, DbError> {
15661 conn.query_row(
15662 "SELECT retain_pinned FROM blob_make_remote_intents
15663 WHERE root_table = ?1 AND root_id = ?2",
15664 (root_table, root_id),
15665 |row| row.get(0),
15666 )
15667 .optional()
15668 .map_err(DbError::from)
15669 }
15670
15671 pub(crate) async fn external_blob_for_row(
15672 &self,
15673 reference: &RowBlobRef,
15674 ) -> Result<Option<ExternalBlob>, DbError> {
15675 let table = reference.table().to_string();
15676 let row_id = reference.row_id().to_string();
15677 let column = reference.column().to_string();
15678 let row_stamp = reference.row_stamp().to_string();
15679 let namespace = reference.blob().namespace.clone();
15680 let blob_id = reference.blob().id.clone();
15681 let expected_size = reference.plaintext_size();
15682 let expected_hash = reference.plaintext_hash();
15683 self.call(move |conn| {
15684 let row = conn
15685 .query_row(
15686 "SELECT path, plaintext_size, plaintext_hash, namespace, blob_id
15687 FROM local_blob_refs
15688 WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3
15689 AND row_stamp = ?4",
15690 rusqlite::params![table, row_id, column, row_stamp],
15691 |row| {
15692 Ok((
15693 row.get::<_, String>(0)?,
15694 row.get::<_, i64>(1)?,
15695 row.get::<_, String>(2)?,
15696 row.get::<_, String>(3)?,
15697 row.get::<_, String>(4)?,
15698 ))
15699 },
15700 )
15701 .optional()
15702 .map_err(DbError::from)?;
15703 let Some((path, size, hash, stored_namespace, stored_blob_id)) = row else {
15704 return Ok(None);
15705 };
15706 let size = u64::try_from(size).map_err(|_| {
15707 DbError::Message(format!("external blob {blob_id} has negative size"))
15708 })?;
15709 let hash: ObjectHash = hash.parse().map_err(|error| {
15710 DbError::Message(format!("external blob {blob_id} hash: {error}"))
15711 })?;
15712 if size != expected_size
15713 || hash != expected_hash
15714 || stored_namespace != namespace
15715 || stored_blob_id != blob_id
15716 {
15717 return Err(DbError::Message(format!(
15718 "external blob row {table}/{row_id}/{column} differs from its row reference"
15719 )));
15720 }
15721 Ok(Some(ExternalBlob {
15722 path: PathBuf::from(path),
15723 size,
15724 }))
15725 })
15726 .await
15727 }
15728}
15729
15730fn validate_snapshot_object_owners_on(
15731 conn: &Connection,
15732 root: &crate::sync::store_commit::StoreRootRef,
15733 meta: &SnapshotMeta,
15734) -> Result<(), DbError> {
15735 let expected = crate::sync::remote_object::SnapshotObjectOwner {
15736 activation: StreamActivationId::store_snapshots(root, &meta.author_registration),
15737 sequence: meta.sequence,
15738 };
15739 if meta.successor.activation != expected.activation {
15740 return Err(DbError::Message(
15741 "verified snapshot successor differs from its author stream activation".to_string(),
15742 ));
15743 }
15744 validate_snapshot_object_owner_records_on(conn, &expected)
15745}
15746
15747fn validate_snapshot_object_owner_records_on(
15748 conn: &Connection,
15749 expected: &crate::sync::remote_object::SnapshotObjectOwner,
15750) -> Result<(), DbError> {
15751 let mut statement = conn
15752 .prepare("SELECT object_id FROM remote_objects ORDER BY object_id")
15753 .map_err(DbError::from)?;
15754 let object_ids = statement
15755 .query_map([], |row| row.get::<_, String>(0))
15756 .map_err(DbError::from)?
15757 .collect::<Result<Vec<_>, _>>()
15758 .map_err(DbError::from)?;
15759 drop(statement);
15760 for object_id in object_ids {
15761 let parsed = object_id.parse().map_err(|error| {
15762 DbError::Message(format!("snapshot remote object id {object_id:?}: {error}"))
15763 })?;
15764 let remote = load_remote_object_on(conn, parsed)?;
15765 for owner in remote.snapshot_owners() {
15766 if owner != expected {
15767 return Err(DbError::Message(format!(
15768 "snapshot remote object {object_id} belongs to a different snapshot stream activation or sequence"
15769 )));
15770 }
15771 }
15772 }
15773 Ok(())
15774}
15775
15776fn validate_snapshot_blob_plan_on(
15777 conn: &Connection,
15778 gates: &Gates,
15779 synced_tables: &[SyncedTable],
15780 meta: &SnapshotMeta,
15781 blob: &PreparedSnapshotBlob,
15782) -> Result<(), DbError> {
15783 blob.remote
15784 .validate()
15785 .map_err(|error| DbError::Message(format!("snapshot remote blob: {error}")))?;
15786 let expected_owner = crate::sync::remote_object::SnapshotObjectOwner {
15787 activation: meta.successor.activation,
15788 sequence: meta.sequence,
15789 };
15790 let owners = blob.remote.snapshot_owners().collect::<Vec<_>>();
15791 if owners != [&expected_owner] {
15792 return Err(DbError::Message(
15793 "snapshot blob owner differs from the verified snapshot stream activation".to_string(),
15794 ));
15795 }
15796 if blob.bindings.is_empty()
15797 || blob.bindings.iter().any(|binding| {
15798 binding.blob().object() != blob.remote.object()
15799 || binding.blob().locator().audience() != blob.authority.remote_audience()
15800 })
15801 || blob
15802 .spool_path
15803 .as_ref()
15804 .is_some_and(|path| !path.is_absolute())
15805 {
15806 return Err(DbError::Message(
15807 "snapshot blob plan has inconsistent exact references".to_string(),
15808 ));
15809 }
15810 for binding in &blob.bindings {
15811 let table = synced_tables
15812 .iter()
15813 .find(|table| table.name() == binding.table())
15814 .ok_or_else(|| {
15815 DbError::Message(format!(
15816 "snapshot blob names undeclared table {:?}",
15817 binding.table()
15818 ))
15819 })?;
15820 let declaration = table.blob().ok_or_else(|| {
15821 DbError::Message(format!(
15822 "snapshot blob names table {:?} without a blob declaration",
15823 table.name()
15824 ))
15825 })?;
15826 let row =
15827 live_blob_row(conn, table.name(), binding.row_id(), declaration)?.ok_or_else(|| {
15828 DbError::Message(format!(
15829 "snapshot blob row {:?}/{:?} is absent",
15830 table.name(),
15831 binding.row_id()
15832 ))
15833 })?;
15834 let audience = gate::live_row_audience(conn, gates, table.name(), binding.row_id())
15835 .map_err(|error| DbError::Message(error.to_string()))?;
15836 let audience = RemoteAudience::try_from(audience)
15837 .map_err(|error| DbError::Message(error.to_string()))?;
15838 validate_live_blob_locator(
15839 binding.table(),
15840 binding.row_id(),
15841 binding.column(),
15842 binding.row_stamp(),
15843 binding.blob(),
15844 declaration,
15845 &row,
15846 &audience,
15847 )?;
15848 }
15849 Ok(())
15850}
15851
15852fn install_snapshot_blob_plan_on(
15853 conn: &Connection,
15854 blob: &PreparedSnapshotBlob,
15855) -> Result<(), DbError> {
15856 let object_id = blob.remote.object_id();
15857 let exists: bool = conn
15858 .query_row(
15859 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
15860 [object_id.to_string()],
15861 |row| row.get(0),
15862 )
15863 .map_err(DbError::from)?;
15864 let merged = if exists {
15865 let mut existing = load_remote_object_on(conn, object_id)?;
15866 for owner in blob.remote.snapshot_owners() {
15867 existing
15868 .merge_snapshot_owner(blob.bindings[0].blob(), owner.clone())
15869 .map_err(|error| DbError::Message(format!("merge snapshot blob owner: {error}")))?;
15870 }
15871 existing
15872 } else {
15873 blob.remote.clone()
15874 };
15875 let encoded = serde_json::to_string(&merged)
15876 .map_err(|error| DbError::Message(format!("serialize snapshot blob: {error}")))?;
15877 conn.execute(
15878 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)
15879 ON CONFLICT(object_id) DO UPDATE SET state = excluded.state",
15880 rusqlite::params![object_id.to_string(), encoded],
15881 )
15882 .map_err(DbError::from)?;
15883 conn.execute(
15884 "INSERT INTO blob_locators (remote_object_id, locator_hash) VALUES (?1, ?2)
15885 ON CONFLICT(remote_object_id) DO NOTHING",
15886 rusqlite::params![
15887 object_id.to_string(),
15888 blob.bindings[0].blob().locator().locator_hash().to_string(),
15889 ],
15890 )
15891 .map_err(DbError::from)?;
15892 validate_stored_locator_on(conn, blob.bindings[0].blob())?;
15893 let authority = serde_json::to_string(&blob.authority)
15894 .map_err(|error| DbError::Message(format!("serialize snapshot blob authority: {error}")))?;
15895 for binding in &blob.bindings {
15896 conn.execute(
15897 "INSERT INTO row_blob_locators
15898 (table_name, row_id, column_name, row_stamp, audience_authority, remote_object_id)
15899 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
15900 ON CONFLICT(table_name, row_id, column_name, row_stamp) DO NOTHING",
15901 rusqlite::params![
15902 binding.table(),
15903 binding.row_id(),
15904 binding.column(),
15905 binding.row_stamp(),
15906 authority,
15907 object_id.to_string(),
15908 ],
15909 )
15910 .map_err(DbError::from)?;
15911 validate_stored_row_binding_on(conn, binding, &blob.authority, object_id)?;
15912 }
15913 Ok(())
15914}
15915
15916enum OutboxIdentity {
15917 Upload {
15918 table: String,
15919 row_id: String,
15920 column: String,
15921 row_stamp: String,
15922 },
15923 Stored {
15924 operation: &'static str,
15925 stored: String,
15926 },
15927}
15928
15929fn upload_entry_for_identity_on(
15930 conn: &Connection,
15931 table: &str,
15932 row_id: &str,
15933 column: &str,
15934 row_stamp: &str,
15935) -> Result<Option<OutboxEntry>, DbError> {
15936 conn.query_row(
15937 "SELECT id, operation, row_ref, stored_ref, source_path, retain_pinned,
15938 upload_state, attempt_count, last_attempt_at, root_table, root_id
15939 FROM cloud_outbox
15940 WHERE operation = 'upload' AND table_name = ?1 AND row_id = ?2
15941 AND column_name = ?3 AND row_stamp = ?4",
15942 rusqlite::params![table, row_id, column, row_stamp],
15943 row_to_outbox_entry,
15944 )
15945 .optional()
15946 .map_err(DbError::from)
15947}
15948
15949fn consume_created_upload_handoff_on(
15950 conn: &Connection,
15951 package: &AudiencePackage,
15952 binding: &RowBlobLocatorBinding,
15953) -> Result<bool, DbError> {
15954 let Some(entry) = upload_entry_for_identity_on(
15955 conn,
15956 binding.table(),
15957 binding.row_id(),
15958 binding.column(),
15959 binding.row_stamp(),
15960 )?
15961 else {
15962 return Ok(false);
15963 };
15964 let OutboxOperation::Upload {
15965 row,
15966 state: OutboxUploadState::Created {
15967 authority, stored, ..
15968 },
15969 ..
15970 } = &entry.operation
15971 else {
15972 return Err(DbError::Message(format!(
15973 "activated blob binding {}/{}/{} at {} has an upload that is not Created",
15974 binding.table(),
15975 binding.row_id(),
15976 binding.column(),
15977 binding.row_stamp()
15978 )));
15979 };
15980 if row.table() != binding.table()
15981 || row.row_id() != binding.row_id()
15982 || row.column() != binding.column()
15983 || row.row_stamp() != binding.row_stamp()
15984 || authority != package.audience()
15985 || stored != binding.blob()
15986 {
15987 return Err(DbError::Message(format!(
15988 "activated blob binding {}/{}/{} at {} differs from its Created upload handoff",
15989 binding.table(),
15990 binding.row_id(),
15991 binding.column(),
15992 binding.row_stamp()
15993 )));
15994 }
15995 Database::remove_cloud_outbox_entry_on(conn, &entry)?;
15996 Ok(true)
15997}
15998
15999fn created_upload_handoff_on(
16000 conn: &Connection,
16001 table: &str,
16002 row_id: &str,
16003 column: &str,
16004 row_stamp: &str,
16005) -> Result<Option<StoreWriteRemoteBlob>, DbError> {
16006 let Some(entry) = upload_entry_for_identity_on(conn, table, row_id, column, row_stamp)? else {
16007 return Ok(None);
16008 };
16009 let OutboxOperation::Upload { row, state, .. } = entry.operation else {
16010 return Err(DbError::Message(
16011 "upload identity query returned a non-upload operation".to_string(),
16012 ));
16013 };
16014 if row.table() != table
16015 || row.row_id() != row_id
16016 || row.column() != column
16017 || row.row_stamp() != row_stamp
16018 {
16019 return Err(DbError::Message(format!(
16020 "upload outbox row facts differ from identity {table}/{row_id}/{column} at {row_stamp}"
16021 )));
16022 }
16023 match state {
16024 OutboxUploadState::Created {
16025 authority, stored, ..
16026 } => Ok(Some(StoreWriteRemoteBlob { authority, stored })),
16027 OutboxUploadState::Pending | OutboxUploadState::Prepared { .. } => Ok(None),
16028 }
16029}
16030
16031fn outbox_identity(operation: &OutboxOperation) -> Result<OutboxIdentity, DbError> {
16032 match operation {
16033 OutboxOperation::Upload { row, .. } => Ok(OutboxIdentity::Upload {
16034 table: row.table().to_string(),
16035 row_id: row.row_id().to_string(),
16036 column: row.column().to_string(),
16037 row_stamp: row.row_stamp().to_string(),
16038 }),
16039 OutboxOperation::Delete { stored } => Ok(OutboxIdentity::Stored {
16040 operation: "delete",
16041 stored: serde_json::to_string(stored).map_err(|error| {
16042 DbError::Message(format!("serialize stored blob outbox identity: {error}"))
16043 })?,
16044 }),
16045 }
16046}
16047
16048fn row_to_outbox_entry(row: &rusqlite::Row<'_>) -> rusqlite::Result<OutboxEntry> {
16049 fn invalid(index: usize, message: String) -> rusqlite::Error {
16050 rusqlite::Error::FromSqlConversionFailure(
16051 index,
16052 rusqlite::types::Type::Text,
16053 Box::new(std::io::Error::new(
16054 std::io::ErrorKind::InvalidData,
16055 message,
16056 )),
16057 )
16058 }
16059
16060 let tag: String = row.get(1)?;
16061 let operation = match tag.as_str() {
16062 "upload" => {
16063 let encoded: String = row.get(2)?;
16064 let reference: RowBlobRef =
16065 serde_json::from_str(&encoded).map_err(|error| invalid(2, error.to_string()))?;
16066 let source_path: String = row.get(4)?;
16067 let state_json: String = row.get(6)?;
16068 let state: OutboxUploadState =
16069 serde_json::from_str(&state_json).map_err(|error| invalid(6, error.to_string()))?;
16070 if let OutboxUploadState::Prepared {
16071 authority, stored, ..
16072 }
16073 | OutboxUploadState::Created {
16074 authority, stored, ..
16075 } = &state
16076 {
16077 let locator = stored.locator();
16078 if locator.namespace() != reference.blob().namespace
16079 || locator.blob_id() != reference.blob().id
16080 || locator.plaintext_size() != reference.plaintext_size()
16081 || locator.plaintext_hash() != reference.plaintext_hash()
16082 || locator
16083 .scope()
16084 .is_some_and(|scope| scope != &reference.blob().scope)
16085 {
16086 return Err(invalid(
16087 6,
16088 "prepared upload differs from its exact row version".to_string(),
16089 ));
16090 }
16091 if locator.audience() != authority.remote_audience() {
16092 return Err(invalid(
16093 6,
16094 "upload package authority differs from its stored locator".to_string(),
16095 ));
16096 }
16097 }
16098 OutboxOperation::Upload {
16099 root_table: row.get(9)?,
16100 root_id: row.get(10)?,
16101 row: reference,
16102 source_path: PathBuf::from(source_path),
16103 retain_pinned: row.get(5)?,
16104 state,
16105 }
16106 }
16107 "delete" => {
16108 let encoded: String = row.get(3)?;
16109 let stored: StoredBlobRef =
16110 serde_json::from_str(&encoded).map_err(|error| invalid(3, error.to_string()))?;
16111 OutboxOperation::Delete { stored }
16112 }
16113 _ => {
16114 return Err(invalid(
16115 1,
16116 format!("invalid cloud outbox operation {tag:?}"),
16117 ))
16118 }
16119 };
16120 Ok(OutboxEntry {
16121 id: row.get(0)?,
16122 attempt_count: row.get(7)?,
16123 last_attempt_at: row.get(8)?,
16124 operation,
16125 })
16126}
16127
16128fn local_store_authority_on(
16129 conn: &Connection,
16130) -> Result<
16131 (
16132 crate::sync::store_commit::StoreRootRef,
16133 StoreDeviceRegistrationRef,
16134 StoreDeviceRegistration,
16135 ),
16136 DbError,
16137> {
16138 let root = required_store_root_authority_on(conn)?;
16139 let device_id: String = conn
16140 .query_row(
16141 "SELECT value FROM protocol_state WHERE key = ?1",
16142 [LOCAL_DEVICE_ID_STATE_KEY],
16143 |row| row.get(0),
16144 )
16145 .map_err(DbError::from)?;
16146 let (bytes, encoded): (Vec<u8>, String) = conn
16147 .query_row(
16148 "SELECT registration_bytes, registration_object \
16149 FROM store_device_registration_activations WHERE device_id = ?1",
16150 [&device_id],
16151 |row| Ok((row.get(0)?, row.get(1)?)),
16152 )
16153 .map_err(DbError::from)?;
16154 let reference: StoreDeviceRegistrationRef =
16155 serde_json::from_str(&encoded).map_err(|error| {
16156 DbError::Message(format!("local Store registration exact ref: {error}"))
16157 })?;
16158 if reference.device_id.to_string() != device_id {
16159 return Err(DbError::Message(
16160 "local Store device state differs from its activated registration".to_string(),
16161 ));
16162 }
16163 let registration = StoreDeviceRegistration::parse_at(&bytes, &root, reference.device_id)
16164 .map_err(|error| DbError::Message(format!("local Store registration: {error}")))?;
16165 reference
16166 .verify_registration(®istration)
16167 .map_err(|error| DbError::Message(error.to_string()))?;
16168 Ok((root, reference, registration))
16169}
16170
16171fn local_merge_stream_id_on(conn: &Connection) -> Result<Option<String>, DbError> {
16172 let local_device_id = conn
16173 .query_row(
16174 "SELECT value FROM protocol_state WHERE key = ?1",
16175 [LOCAL_DEVICE_ID_STATE_KEY],
16176 |row| row.get::<_, String>(0),
16177 )
16178 .optional()
16179 .map_err(DbError::from)?;
16180 let Some(_) = local_device_id else {
16181 return Ok(None);
16182 };
16183 let (root, reference, _) = local_store_authority_on(conn)?;
16184 Ok(Some(
16185 AuthorStreamId::store_announcements(&root, &reference).to_string(),
16186 ))
16187}
16188
16189fn pin_host_device_id_on(
16190 conn: &Connection,
16191 expected: &str,
16192 initialized: bool,
16193) -> Result<(), DbError> {
16194 if initialized {
16195 return validate_host_device_id_on(conn, expected);
16196 }
16197 conn.execute(
16198 "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)",
16199 (HOST_DEVICE_ID_STATE_KEY, expected),
16200 )
16201 .map(|_| ())
16202 .map_err(DbError::from)
16203}
16204
16205fn validate_host_device_id_on(conn: &Connection, expected: &str) -> Result<(), DbError> {
16206 let stored = conn
16207 .query_row(
16208 "SELECT value FROM protocol_state WHERE key = ?1",
16209 [HOST_DEVICE_ID_STATE_KEY],
16210 |row| row.get::<_, String>(0),
16211 )
16212 .optional()
16213 .map_err(DbError::from)?
16214 .ok_or_else(|| DbError::Message("Coven host device identity is absent".to_string()))?;
16215 if stored != expected {
16216 return Err(DbError::Message(format!(
16217 "Coven host device identity is {stored:?}, not configured identity {expected:?}"
16218 )));
16219 }
16220 Ok(())
16221}
16222
16223fn store_ack_first_slot(
16224 registration: &StoreDeviceRegistration,
16225) -> Result<&crate::storage::cloud::ObjectSlot, DbError> {
16226 match ®istration.acknowledgements {
16227 crate::sync::store_commit::DeviceStreamAnchor::StoreAcknowledgements { first_slot } => {
16228 Ok(first_slot)
16229 }
16230 _ => Err(DbError::Message(
16231 "local Store registration has no acknowledgement stream anchor".to_string(),
16232 )),
16233 }
16234}
16235
16236fn store_snapshot_first_slot(
16237 registration: &StoreDeviceRegistration,
16238) -> Result<&crate::storage::cloud::ObjectSlot, DbError> {
16239 match ®istration.snapshots {
16240 crate::sync::store_commit::DeviceStreamAnchor::StoreSnapshots { first_slot } => {
16241 Ok(first_slot)
16242 }
16243 _ => Err(DbError::Message(
16244 "local Store registration has no snapshot stream anchor".to_string(),
16245 )),
16246 }
16247}
16248
16249fn load_published_store_ack_on(conn: &Connection) -> Result<Option<PublishedStoreAck>, DbError> {
16250 conn.query_row(
16251 "SELECT ack_ref, successor_slot FROM published_store_acks WHERE singleton = 1",
16252 [],
16253 |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
16254 )
16255 .optional()
16256 .map_err(DbError::from)?
16257 .map(|(reference, successor_slot)| {
16258 let reference: StoreAckRef = serde_json::from_str(&reference).map_err(|error| {
16259 DbError::Message(format!("published Store acknowledgement ref: {error}"))
16260 })?;
16261 if reference.revision == 0 {
16262 return Err(DbError::Message(
16263 "published Store acknowledgement uses revision zero".to_string(),
16264 ));
16265 }
16266 Ok(PublishedStoreAck {
16267 reference,
16268 successor_slot: serde_json::from_str(&successor_slot).map_err(|error| {
16269 DbError::Message(format!(
16270 "published Store acknowledgement successor slot: {error}"
16271 ))
16272 })?,
16273 })
16274 })
16275 .transpose()
16276}
16277
16278fn load_outbound_store_ack_on(conn: &Connection) -> Result<Option<OutboundStoreAck>, DbError> {
16279 conn.query_row(
16280 "SELECT ack_ref, ack_bytes, prepared_object \
16281 FROM outbound_store_acks WHERE singleton = 1",
16282 [],
16283 |row| {
16284 Ok((
16285 row.get::<_, String>(0)?,
16286 row.get::<_, Vec<u8>>(1)?,
16287 row.get::<_, String>(2)?,
16288 ))
16289 },
16290 )
16291 .optional()
16292 .map_err(DbError::from)?
16293 .map(|(reference, bytes, prepared)| {
16294 let reference: StoreAckRef = serde_json::from_str(&reference).map_err(|error| {
16295 DbError::Message(format!("outbound Store acknowledgement ref: {error}"))
16296 })?;
16297 let prepared: PreparedExactObject = serde_json::from_str(&prepared).map_err(|error| {
16298 DbError::Message(format!("outbound prepared Store acknowledgement: {error}"))
16299 })?;
16300 if prepared.reference() != &reference.object {
16301 return Err(DbError::Message(
16302 "outbound Store acknowledgement ref differs from its prepared object".to_string(),
16303 ));
16304 }
16305 let (root, author_ref, author) = local_store_authority_on(conn)?;
16306 let value = StoreAck::parse_at(&bytes, root.store_root_hash, &reference, &author).map_err(
16307 |error| DbError::Message(format!("outbound Store acknowledgement: {error}")),
16308 )?;
16309 if value.author_registration != author_ref {
16310 return Err(DbError::Message(
16311 "outbound Store acknowledgement author differs from local activation".to_string(),
16312 ));
16313 }
16314 Ok(OutboundStoreAck {
16315 reference,
16316 ack: ExactProtocolObject {
16317 value,
16318 bytes,
16319 object: prepared.reference().clone(),
16320 prepared,
16321 },
16322 })
16323 })
16324 .transpose()
16325}
16326
16327fn load_published_store_snapshot_on(
16328 conn: &Connection,
16329) -> Result<Option<PublishedStoreSnapshot>, DbError> {
16330 conn.query_row(
16331 "SELECT snapshot_ref, successor_slot, meta_bytes \
16332 FROM published_store_snapshot WHERE singleton = 1",
16333 [],
16334 |row| {
16335 Ok((
16336 row.get::<_, String>(0)?,
16337 row.get::<_, String>(1)?,
16338 row.get::<_, Vec<u8>>(2)?,
16339 ))
16340 },
16341 )
16342 .optional()
16343 .map_err(DbError::from)?
16344 .map(|(reference, successor_slot, bytes)| {
16345 let reference: StoreSnapshotRef = serde_json::from_str(&reference)
16346 .map_err(|error| DbError::Message(format!("published Store snapshot ref: {error}")))?;
16347 let successor_slot = serde_json::from_str(&successor_slot).map_err(|error| {
16348 DbError::Message(format!("published Store snapshot successor slot: {error}"))
16349 })?;
16350 let (root, author_ref, author) = local_store_authority_on(conn)?;
16351 let meta = SnapshotMeta::parse_at(&bytes, root.store_root_hash, &reference, &author)
16352 .map_err(|error| DbError::Message(format!("published Store snapshot: {error}")))?;
16353 if meta.author_registration != author_ref || meta.successor.next_slot != successor_slot {
16354 return Err(DbError::Message(
16355 "published Store snapshot differs from its local stream state".to_string(),
16356 ));
16357 }
16358 Ok(PublishedStoreSnapshot {
16359 reference,
16360 successor_slot,
16361 meta,
16362 })
16363 })
16364 .transpose()
16365}
16366
16367fn load_outbound_store_snapshot_on(
16368 conn: &Connection,
16369) -> Result<Option<DurableSnapshotPublication>, DbError> {
16370 conn.query_row(
16371 "SELECT snapshot_ref, meta_prepared, image_ref, image_prepared, image_bytes, meta_bytes, blobs \
16372 FROM outbound_store_snapshot WHERE singleton = 1",
16373 [],
16374 |row| {
16375 Ok((
16376 row.get::<_, String>(0)?,
16377 row.get::<_, String>(1)?,
16378 row.get::<_, String>(2)?,
16379 row.get::<_, String>(3)?,
16380 row.get::<_, Vec<u8>>(4)?,
16381 row.get::<_, Vec<u8>>(5)?,
16382 row.get::<_, String>(6)?,
16383 ))
16384 },
16385 )
16386 .optional()
16387 .map_err(DbError::from)?
16388 .map(
16389 |(reference, meta_prepared, image_reference, image_prepared, image_bytes, meta_bytes, blobs)| {
16390 let reference: StoreSnapshotRef =
16391 serde_json::from_str(&reference).map_err(|error| {
16392 DbError::Message(format!("outbound Store snapshot ref: {error}"))
16393 })?;
16394 let meta_prepared: PreparedExactObject =
16395 serde_json::from_str(&meta_prepared).map_err(|error| {
16396 DbError::Message(format!(
16397 "outbound prepared Store snapshot metadata: {error}"
16398 ))
16399 })?;
16400 let image_reference: SnapshotImageRef = serde_json::from_str(&image_reference)
16401 .map_err(|error| {
16402 DbError::Message(format!("outbound Store snapshot image ref: {error}"))
16403 })?;
16404 let image_prepared: PreparedExactObject = serde_json::from_str(&image_prepared)
16405 .map_err(|error| {
16406 DbError::Message(format!("outbound prepared Store snapshot image: {error}"))
16407 })?;
16408 let blobs: Vec<PreparedSnapshotBlob> = serde_json::from_str(&blobs).map_err(|error| {
16409 DbError::Message(format!("outbound prepared Store snapshot blobs: {error}"))
16410 })?;
16411 if meta_prepared.reference() != &reference.object
16412 || image_prepared.reference() != &image_reference.object
16413 || ObjectHash::digest(&image_bytes) != image_reference.image_hash
16414 {
16415 return Err(DbError::Message(
16416 "outbound Store snapshot exact references differ from prepared bytes"
16417 .to_string(),
16418 ));
16419 }
16420 let (root, author_ref, author) = local_store_authority_on(conn)?;
16421 let meta =
16422 SnapshotMeta::parse_at(&meta_bytes, root.store_root_hash, &reference, &author)
16423 .map_err(|error| {
16424 DbError::Message(format!("outbound Store snapshot: {error}"))
16425 })?;
16426 if meta.author_registration != author_ref || meta.image != image_reference {
16427 return Err(DbError::Message(
16428 "outbound Store snapshot metadata differs from its exact image".to_string(),
16429 ));
16430 }
16431 Ok(DurableSnapshotPublication {
16432 reference,
16433 meta: ExactProtocolObject {
16434 value: meta,
16435 bytes: meta_bytes,
16436 object: meta_prepared.reference().clone(),
16437 prepared: meta_prepared,
16438 },
16439 image: ExactProtocolObject {
16440 value: image_bytes.clone(),
16441 bytes: image_bytes,
16442 object: image_prepared.reference().clone(),
16443 prepared: image_prepared,
16444 },
16445 blobs,
16446 })
16447 },
16448 )
16449 .transpose()
16450}
16451
16452struct LiveBlobRow {
16453 stamp: String,
16454 blob_id: String,
16455 plaintext_size: u64,
16456 plaintext_hash: ObjectHash,
16457 cloud_path: Option<String>,
16458}
16459
16460fn live_blob_row(
16461 conn: &Connection,
16462 table: &str,
16463 row_id: &str,
16464 declaration: &crate::sync::session::BlobDecl,
16465) -> Result<Option<LiveBlobRow>, DbError> {
16466 let cloud_path = declaration
16467 .cloud_path_column
16468 .as_deref()
16469 .map(quote_ident)
16470 .unwrap_or_else(|| "NULL".to_string());
16471 let sql = format!(
16472 "SELECT {}, {}, {}, {}, {} FROM {} WHERE {} = ?1",
16473 quote_ident(&declaration.id_column),
16474 quote_ident(&declaration.size_column),
16475 quote_ident(&declaration.hash_column),
16476 cloud_path,
16477 quote_ident("_updated_at"),
16478 quote_ident(table),
16479 quote_ident("id"),
16480 );
16481 let raw = conn
16482 .query_row(&sql, [row_id], |row| {
16483 Ok((
16484 row.get::<_, String>(0)?,
16485 row.get::<_, i64>(1)?,
16486 row.get::<_, String>(2)?,
16487 row.get::<_, Option<String>>(3)?,
16488 row.get::<_, String>(4)?,
16489 ))
16490 })
16491 .optional()
16492 .map_err(DbError::from)?;
16493 let Some((blob_id, plaintext_size, plaintext_hash, cloud_path, stamp)) = raw else {
16494 return Ok(None);
16495 };
16496 let plaintext_size = u64::try_from(plaintext_size).map_err(|_| {
16497 DbError::Message(format!(
16498 "winning blob row {:?}/{:?} has negative plaintext size {plaintext_size}",
16499 table, row_id
16500 ))
16501 })?;
16502 let plaintext_hash = plaintext_hash.parse().map_err(|error| {
16503 DbError::Message(format!(
16504 "winning blob row {:?}/{:?} has invalid plaintext hash: {error}",
16505 table, row_id
16506 ))
16507 })?;
16508 Ok(Some(LiveBlobRow {
16509 stamp,
16510 blob_id,
16511 plaintext_size,
16512 plaintext_hash,
16513 cloud_path,
16514 }))
16515}
16516
16517fn validate_live_blob_row(
16518 binding: &RowBlobLocatorBinding,
16519 declaration: &crate::sync::session::BlobDecl,
16520 row: &LiveBlobRow,
16521 live_audience: &RemoteAudience,
16522) -> Result<(), DbError> {
16523 validate_live_blob_locator(
16524 binding.table(),
16525 binding.row_id(),
16526 binding.column(),
16527 binding.row_stamp(),
16528 binding.blob(),
16529 declaration,
16530 row,
16531 live_audience,
16532 )
16533}
16534
16535#[allow(clippy::too_many_arguments)]
16536fn validate_live_blob_locator(
16537 table: &str,
16538 row_id: &str,
16539 column: &str,
16540 row_stamp: &str,
16541 stored: &StoredBlobRef,
16542 declaration: &crate::sync::session::BlobDecl,
16543 row: &LiveBlobRow,
16544 live_audience: &RemoteAudience,
16545) -> Result<(), DbError> {
16546 let locator = stored.locator();
16547 let invalid = locator.namespace() != declaration.namespace
16548 || locator.blob_id() != row.blob_id
16549 || locator.plaintext_size() != row.plaintext_size
16550 || locator.plaintext_hash() != row.plaintext_hash
16551 || &locator.audience() != live_audience
16552 || locator
16553 .scope()
16554 .is_some_and(|scope| scope != &declaration.scope)
16555 || locator
16556 .cloud_path()
16557 .is_some_and(|path| row.cloud_path.as_deref() != Some(path));
16558 if invalid {
16559 return Err(DbError::Message(format!(
16560 "blob locator does not match winning row values for {:?}/{:?}/{:?} at {:?}",
16561 table, row_id, column, row_stamp
16562 )));
16563 }
16564 Ok(())
16565}
16566
16567fn validate_stored_locator_on(conn: &Connection, expected: &StoredBlobRef) -> Result<(), DbError> {
16568 let locator_hash = expected.locator().locator_hash().to_string();
16569 let expected_remote_object_id = remote_object_id(expected.object());
16570 let stored_locator_hash: String = conn
16571 .query_row(
16572 "SELECT locator_hash FROM blob_locators WHERE remote_object_id = ?1",
16573 [expected_remote_object_id.to_string()],
16574 |row| row.get(0),
16575 )
16576 .map_err(DbError::from)?;
16577 if stored_locator_hash != locator_hash {
16578 return Err(DbError::Message(format!(
16579 "stored blob object {expected_remote_object_id} is indexed under locator {stored_locator_hash}, expected {locator_hash}"
16580 )));
16581 }
16582 let remote = load_remote_object_on(conn, expected_remote_object_id)?;
16583 if !remote.is_activated_stored_blob() {
16584 return Err(DbError::Message(format!(
16585 "stored blob locator {locator_hash} does not reference activated ownership"
16586 )));
16587 }
16588 let locator =
16589 BlobLocator::parse(remote.bytes().canonical_semantic_bytes()).map_err(|error| {
16590 DbError::Message(format!(
16591 "stored blob locator {locator_hash} is invalid: {error}"
16592 ))
16593 })?;
16594 let actual = StoredBlobRef::new(locator, remote.object().clone()).map_err(|error| {
16595 DbError::Message(format!(
16596 "stored blob reference {locator_hash} is invalid: {error}"
16597 ))
16598 })?;
16599 if &actual != expected {
16600 return Err(DbError::Message(format!(
16601 "blob object {expected_remote_object_id} differs from its exact stored reference"
16602 )));
16603 }
16604 Ok(())
16605}
16606
16607fn validate_stored_row_binding_on(
16608 conn: &Connection,
16609 binding: &RowBlobLocatorBinding,
16610 expected_authority: &crate::sync::audience_package::PackageAudience,
16611 expected_remote_object_id: ObjectHash,
16612) -> Result<(), DbError> {
16613 let (audience_authority, remote_object_id): (String, String) = conn
16614 .query_row(
16615 "SELECT audience_authority, remote_object_id FROM row_blob_locators
16616 WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3 AND row_stamp = ?4",
16617 rusqlite::params![
16618 binding.table(),
16619 binding.row_id(),
16620 binding.column(),
16621 binding.row_stamp(),
16622 ],
16623 |row| Ok((row.get(0)?, row.get(1)?)),
16624 )
16625 .map_err(DbError::from)?;
16626 let actual_authority: crate::sync::audience_package::PackageAudience =
16627 serde_json::from_str(&audience_authority).map_err(|error| {
16628 DbError::Message(format!("parse stored row blob audience authority: {error}"))
16629 })?;
16630 if &actual_authority != expected_authority
16631 || remote_object_id != expected_remote_object_id.to_string()
16632 {
16633 return Err(DbError::Message(format!(
16634 "row blob binding {:?}/{:?}/{:?} at {:?} is already bound to different exact content",
16635 binding.table(),
16636 binding.row_id(),
16637 binding.column(),
16638 binding.row_stamp()
16639 )));
16640 }
16641 Ok(())
16642}
16643
16644fn load_prepared_audience_objects_on(
16645 conn: &Connection,
16646 write_id: &WriteId,
16647) -> Result<PreparedAudienceObjects, DbError> {
16648 let mut package_statement = conn
16649 .prepare(
16650 "SELECT remote_object_id FROM store_write_packages
16651 WHERE write_id = ?1 ORDER BY audience",
16652 )
16653 .map_err(DbError::from)?;
16654 let package_ids = package_statement
16655 .query_map([write_id.as_str()], |row| row.get::<_, String>(0))
16656 .map_err(DbError::from)?
16657 .collect::<Result<Vec<_>, _>>()
16658 .map_err(DbError::from)?;
16659 let mut blob_statement = conn
16660 .prepare(
16661 "SELECT remote_object_id, audience, locator_hash, spool_path FROM store_write_blobs
16662 WHERE write_id = ?1 ORDER BY audience, remote_object_id",
16663 )
16664 .map_err(DbError::from)?;
16665 let blob_rows = blob_statement
16666 .query_map([write_id.as_str()], |row| {
16667 Ok((
16668 row.get::<_, String>(0)?,
16669 row.get::<_, String>(1)?,
16670 row.get::<_, String>(2)?,
16671 row.get::<_, Option<String>>(3)?,
16672 ))
16673 })
16674 .map_err(DbError::from)?
16675 .collect::<Result<Vec<_>, _>>()
16676 .map_err(DbError::from)?;
16677 let packages = package_ids
16678 .into_iter()
16679 .map(|encoded| {
16680 let object_id = encoded
16681 .parse()
16682 .map_err(|error| DbError::Message(format!("stored remote object id: {error}")))?;
16683 PreparedAudiencePackage::from_remote(load_remote_object_on(conn, object_id)?)
16684 })
16685 .collect::<Result<Vec<_>, DbError>>()?;
16686 let blobs = blob_rows
16687 .into_iter()
16688 .map(|(encoded, audience, locator_hash, spool_path)| {
16689 let object_id = encoded
16690 .parse()
16691 .map_err(|error| DbError::Message(format!("stored remote object id: {error}")))?;
16692 PreparedAudienceBlob::from_remote(
16693 parse_remote_audience_db(&audience)?,
16694 &locator_hash,
16695 load_remote_object_on(conn, object_id)?,
16696 spool_path.map(PathBuf::from),
16697 )
16698 })
16699 .collect::<Result<Vec<_>, DbError>>()?;
16700 Ok(PreparedAudienceObjects { packages, blobs })
16701}
16702
16703fn load_activated_registration_on(
16704 conn: &Connection,
16705 root: &crate::sync::store_commit::StoreRootRef,
16706 reference: &StoreDeviceRegistrationRef,
16707) -> Result<StoreDeviceRegistration, DbError> {
16708 let (bytes, encoded): (Vec<u8>, String) = conn
16709 .query_row(
16710 "SELECT registration_bytes, registration_object \
16711 FROM store_device_registration_activations \
16712 WHERE device_id = ?1 AND registration_hash = ?2",
16713 (
16714 reference.device_id.to_string(),
16715 reference.registration_hash.to_string(),
16716 ),
16717 |row| Ok((row.get(0)?, row.get(1)?)),
16718 )
16719 .map_err(DbError::from)?;
16720 let stored: StoreDeviceRegistrationRef = serde_json::from_str(&encoded)
16721 .map_err(|error| DbError::Message(format!("activated Store registration ref: {error}")))?;
16722 if stored != *reference {
16723 return Err(DbError::Message(
16724 "activated Store registration differs from its exact reference".to_string(),
16725 ));
16726 }
16727 let registration = StoreDeviceRegistration::parse_at(&bytes, root, reference.device_id)
16728 .map_err(|error| DbError::Message(format!("activated Store registration: {error}")))?;
16729 reference
16730 .verify_registration(®istration)
16731 .map_err(|error| DbError::Message(error.to_string()))?;
16732 Ok(registration)
16733}
16734
16735#[allow(clippy::too_many_arguments)]
16736pub(crate) fn previous_row_blob_for_write_on(
16737 conn: &Connection,
16738 table: &str,
16739 row_id: &str,
16740 row_stamp: &str,
16741 column: &str,
16742 blob: &BlobRef,
16743 plaintext_size: u64,
16744 plaintext_hash: ObjectHash,
16745) -> Result<Option<StoreWriteRemoteBlob>, DbError> {
16746 if let Some(handoff) = created_upload_handoff_on(conn, table, row_id, column, row_stamp)? {
16747 let locator = handoff.stored.locator();
16748 if locator.namespace() != blob.namespace
16749 || locator.blob_id() != blob.id
16750 || locator.plaintext_size() != plaintext_size
16751 || locator.plaintext_hash() != plaintext_hash
16752 || locator.scope().is_some_and(|scope| scope != &blob.scope)
16753 {
16754 return Err(DbError::Message(format!(
16755 "created upload {table}/{row_id}/{column} at {row_stamp} differs from its captured row"
16756 )));
16757 }
16758 return Ok(Some(handoff));
16759 }
16760 let raw = conn
16761 .query_row(
16762 "SELECT row_blob_locators.audience_authority, blob_locators.remote_object_id
16763 FROM row_blob_locators
16764 JOIN blob_locators USING (remote_object_id)
16765 WHERE table_name = ?1 AND row_id = ?2 AND column_name = ?3
16766 ORDER BY row_stamp DESC LIMIT 1",
16767 rusqlite::params![table, row_id, column],
16768 |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
16769 )
16770 .optional()
16771 .map_err(DbError::from)?;
16772 let Some((authority, object_id)) = raw else {
16773 return Ok(None);
16774 };
16775 let authority: crate::sync::audience_package::PackageAudience =
16776 serde_json::from_str(&authority)
16777 .map_err(|error| DbError::Message(format!("prior row blob authority: {error}")))?;
16778 let object_id = object_id
16779 .parse()
16780 .map_err(|error| DbError::Message(format!("prior row blob object id: {error}")))?;
16781 let remote = load_remote_object_on(conn, object_id)?;
16782 if !remote.is_activated_stored_blob() {
16783 return Err(DbError::Message(format!(
16784 "prior row blob {table}/{row_id}/{column} is not activated"
16785 )));
16786 }
16787 let locator = BlobLocator::parse(remote.bytes().canonical_semantic_bytes())
16788 .map_err(|error| DbError::Message(format!("prior row blob locator: {error}")))?;
16789 if locator.namespace() != blob.namespace
16790 || locator.blob_id() != blob.id
16791 || locator.plaintext_size() != plaintext_size
16792 || locator.plaintext_hash() != plaintext_hash
16793 || locator.scope().is_some_and(|scope| scope != &blob.scope)
16794 {
16795 return Ok(None);
16796 }
16797 if locator.audience() != authority.remote_audience() {
16798 return Err(DbError::Message(format!(
16799 "prior row blob {table}/{row_id}/{column} authority differs from its locator"
16800 )));
16801 }
16802 let stored = StoredBlobRef::new(locator, remote.object().clone())
16803 .map_err(|error| DbError::Message(format!("prior row blob reference: {error}")))?;
16804 Ok(Some(StoreWriteRemoteBlob { authority, stored }))
16805}
16806
16807fn remote_audience_to_db(audience: &RemoteAudience) -> String {
16808 match audience {
16809 RemoteAudience::Store => "store".to_string(),
16810 RemoteAudience::Circle(circle_id) => circle_id.to_string(),
16811 }
16812}
16813
16814fn parse_remote_audience_db(value: &str) -> Result<RemoteAudience, DbError> {
16815 if value == "store" {
16816 return Ok(RemoteAudience::Store);
16817 }
16818 value.parse().map(RemoteAudience::Circle).map_err(|error| {
16819 DbError::Message(format!("invalid stored blob audience {value:?}: {error}"))
16820 })
16821}
16822
16823enum RemoteStoredRepresentationRef<'a> {
16824 Inline(&'a [u8]),
16825 Blob,
16826}
16827
16828fn candidate_manifest_exact_objects(commit: &StoreBatchCommit) -> Vec<&ExactObjectRef> {
16829 let mut objects = Vec::new();
16830 for candidate in &commit.candidate_objects.objects {
16831 match candidate {
16832 crate::sync::store_commit::CandidateExclusiveObjectRef::StorePackage(reference) => {
16833 objects.push(&reference.object);
16834 }
16835 crate::sync::store_commit::CandidateExclusiveObjectRef::CirclePackage(reference) => {
16836 objects.push(&reference.package.object);
16837 }
16838 crate::sync::store_commit::CandidateExclusiveObjectRef::CircleAccess {
16839 access, ..
16840 } => {
16841 objects.push(&access.leaf.object);
16842 objects.push(&access.envelope.object);
16843 }
16844 crate::sync::store_commit::CandidateExclusiveObjectRef::SelfRetirement(reference) => {
16845 objects.push(&reference.object);
16846 }
16847 }
16848 }
16849 objects
16850}
16851
16852fn validate_remote_object_on(
16853 conn: &Connection,
16854 object_id: ObjectHash,
16855 expected_object: &ExactObjectRef,
16856 expected_semantic_bytes: &[u8],
16857 expected_stored: RemoteStoredRepresentationRef<'_>,
16858) -> Result<(), DbError> {
16859 let remote = load_remote_object_on(conn, object_id)?;
16860 let stored = remote.bytes().stored();
16861 let stored_matches = match expected_stored {
16862 RemoteStoredRepresentationRef::Inline(expected) => stored.inline_bytes() == Some(expected),
16863 RemoteStoredRepresentationRef::Blob => stored.inline_bytes().is_none(),
16864 };
16865 if remote.object() != expected_object
16866 || remote.bytes().canonical_semantic_bytes() != expected_semantic_bytes
16867 || !stored_matches
16868 {
16869 return Err(DbError::Message(format!(
16870 "prepared remote object {object_id} differs from its semantic index"
16871 )));
16872 }
16873 Ok(())
16874}
16875
16876fn load_remote_object_on(
16877 conn: &Connection,
16878 object_id: ObjectHash,
16879) -> Result<RemoteObjectRecord, DbError> {
16880 let state: String = conn
16881 .query_row(
16882 "SELECT state FROM remote_objects WHERE object_id = ?1",
16883 [object_id.to_string()],
16884 |row| row.get(0),
16885 )
16886 .map_err(DbError::from)?;
16887 let remote: RemoteObjectRecord = serde_json::from_str(&state).map_err(|error| {
16888 DbError::Message(format!(
16889 "prepared remote object {object_id} has invalid closed state: {error}"
16890 ))
16891 })?;
16892 remote.validate().map_err(|error| {
16893 DbError::Message(format!("prepared remote object {object_id}: {error}"))
16894 })?;
16895 let actual = remote_object_id(remote.object());
16896 if actual != object_id {
16897 return Err(DbError::Message(format!(
16898 "prepared remote object key is {object_id}, exact reference hashes to {actual}"
16899 )));
16900 }
16901 Ok(remote)
16902}
16903
16904fn persist_exact_remote_object_on(
16905 conn: &Connection,
16906 remote: &RemoteObjectRecord,
16907 domain: &str,
16908) -> Result<(), DbError> {
16909 remote
16910 .validate()
16911 .map_err(|error| DbError::Message(format!("prepared {domain}: {error}")))?;
16912 let object_id = remote.object_id();
16913 let existing = conn
16914 .query_row(
16915 "SELECT state FROM remote_objects WHERE object_id = ?1",
16916 [object_id.to_string()],
16917 |row| row.get::<_, String>(0),
16918 )
16919 .optional()
16920 .map_err(DbError::from)?;
16921 if let Some(existing) = existing {
16922 let existing: RemoteObjectRecord = serde_json::from_str(&existing).map_err(|error| {
16923 DbError::Message(format!(
16924 "prepared {domain} {object_id} has invalid closed state: {error}"
16925 ))
16926 })?;
16927 if existing != *remote {
16928 return Err(DbError::Message(format!(
16929 "prepared {domain} {object_id} already has different closed state"
16930 )));
16931 }
16932 return Ok(());
16933 }
16934 let state = serde_json::to_string(remote)
16935 .map_err(|error| DbError::Message(format!("serialize prepared {domain}: {error}")))?;
16936 conn.execute(
16937 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
16938 (object_id.to_string(), state),
16939 )
16940 .map_err(DbError::from)?;
16941 Ok(())
16942}
16943
16944fn update_remote_object_on(
16945 conn: &Connection,
16946 object_id: ObjectHash,
16947 remote: &RemoteObjectRecord,
16948) -> Result<(), DbError> {
16949 remote
16950 .validate()
16951 .map_err(|error| DbError::Message(format!("remote object {object_id}: {error}")))?;
16952 if remote.object_id() != object_id {
16953 return Err(DbError::Message(format!(
16954 "remote object {object_id} changed its exact identity"
16955 )));
16956 }
16957 let state = serde_json::to_string(remote)
16958 .map_err(|error| DbError::Message(format!("serialize remote object: {error}")))?;
16959 let updated = conn
16960 .execute(
16961 "UPDATE remote_objects SET state = ?2 WHERE object_id = ?1",
16962 (object_id.to_string(), state),
16963 )
16964 .map_err(DbError::from)?;
16965 if updated != 1 {
16966 return Err(DbError::Message(format!(
16967 "remote object {object_id} disappeared during state transition"
16968 )));
16969 }
16970 Ok(())
16971}
16972
16973fn mark_remote_object_uploaded_on(
16974 conn: &Connection,
16975 expected: RemoteObjectRecord,
16976) -> Result<RemoteObjectRecord, DbError> {
16977 let object_id = expected.object_id();
16978 let current = load_remote_object_on(conn, object_id)?;
16979 if current != expected {
16980 return Err(DbError::Message(format!(
16981 "remote object {object_id} changed before upload completion"
16982 )));
16983 }
16984 let expected_json = serde_json::to_string(&expected)
16985 .map_err(|error| DbError::Message(format!("serialize expected remote object: {error}")))?;
16986 let mut uploaded = current;
16987 uploaded.mark_uploaded_verified().map_err(|error| {
16988 DbError::Message(format!("mark remote object {object_id} uploaded: {error}"))
16989 })?;
16990 let uploaded_json = serde_json::to_string(&uploaded)
16991 .map_err(|error| DbError::Message(format!("serialize uploaded remote object: {error}")))?;
16992 let updated = conn
16993 .execute(
16994 "UPDATE remote_objects SET state = ?3
16995 WHERE object_id = ?1 AND state = ?2",
16996 (object_id.to_string(), expected_json, uploaded_json),
16997 )
16998 .map_err(DbError::from)?;
16999 if updated != 1 {
17000 return Err(DbError::Message(format!(
17001 "remote object {object_id} lost upload ownership"
17002 )));
17003 }
17004 Ok(uploaded)
17005}
17006
17007fn merge_prepared_remote_object(
17008 existing: RemoteObjectRecord,
17009 proposed: &RemoteObjectRecord,
17010 owner: &StoreBatchCommitRef,
17011) -> Result<RemoteObjectRecord, DbError> {
17012 use crate::sync::remote_object::{OwnedObjectState, SharedLiveSetObjectDomain};
17013
17014 if &existing == proposed {
17015 return Ok(existing);
17016 }
17017 let (
17018 RemoteObjectRecord::SharedLiveSet(mut existing),
17019 RemoteObjectRecord::SharedLiveSet(proposed),
17020 ) = (existing, proposed)
17021 else {
17022 return Err(DbError::Message(format!(
17023 "remote object {} already has different closed state",
17024 proposed.object_id()
17025 )));
17026 };
17027 if existing.identity.domain != SharedLiveSetObjectDomain::StoredBlob
17028 || proposed.identity.domain != SharedLiveSetObjectDomain::StoredBlob
17029 || existing.identity != proposed.identity
17030 || existing.bytes != proposed.bytes
17031 {
17032 return Err(DbError::Message(format!(
17033 "stored blob object {} already has different identity or bytes",
17034 remote_object_id(&proposed.identity.object)
17035 )));
17036 }
17037 let proposed_has_owner = match &proposed.state {
17038 OwnedObjectState::Prepared { ownership } => ownership.pending.contains(owner),
17039 OwnedObjectState::UploadedVerified { ownership } => ownership.pending.contains(owner),
17040 OwnedObjectState::RetirementPending { .. } => false,
17041 };
17042 if !proposed_has_owner {
17043 return Err(DbError::Message(format!(
17044 "stored blob object {} does not name its preparing commit",
17045 remote_object_id(&proposed.identity.object)
17046 )));
17047 }
17048 let proposed_uploaded = matches!(&proposed.state, OwnedObjectState::UploadedVerified { .. });
17049 match &mut existing.state {
17050 OwnedObjectState::Prepared { ownership } => {
17051 ownership.pending.insert(owner.clone());
17052 if proposed_uploaded {
17053 existing.state = OwnedObjectState::UploadedVerified {
17054 ownership: crate::sync::remote_object::SharedObjectOwnership {
17055 pending: ownership.pending.clone(),
17056 activated: std::collections::BTreeSet::new(),
17057 nonactivated: ownership.nonactivated.clone(),
17058 },
17059 };
17060 }
17061 }
17062 OwnedObjectState::UploadedVerified { ownership } => {
17063 ownership.pending.insert(owner.clone());
17064 }
17065 OwnedObjectState::RetirementPending { former_candidates } => {
17066 let ownership = crate::sync::remote_object::PendingCandidateOwnership {
17067 pending: std::collections::BTreeSet::from([owner.clone()]),
17068 nonactivated: former_candidates.clone(),
17069 };
17070 existing.state = if proposed_uploaded {
17071 OwnedObjectState::UploadedVerified {
17072 ownership: crate::sync::remote_object::SharedObjectOwnership {
17073 pending: ownership.pending,
17074 activated: std::collections::BTreeSet::new(),
17075 nonactivated: ownership.nonactivated,
17076 },
17077 }
17078 } else {
17079 OwnedObjectState::Prepared { ownership }
17080 };
17081 }
17082 }
17083 let merged = RemoteObjectRecord::SharedLiveSet(existing);
17084 merged.validate().map_err(|error| {
17085 DbError::Message(format!(
17086 "merged stored blob object {}: {error}",
17087 merged.object_id()
17088 ))
17089 })?;
17090 Ok(merged)
17091}
17092
17093fn validate_prepared_package_on(
17094 conn: &Connection,
17095 write_id: &WriteId,
17096 expected: &PreparedAudiencePackage,
17097) -> Result<(), DbError> {
17098 let audience = expected.package().audience().remote_audience();
17099 let remote_object_id: String = conn
17100 .query_row(
17101 "SELECT remote_object_id
17102 FROM store_write_packages
17103 WHERE write_id = ?1 AND audience = ?2",
17104 rusqlite::params![write_id.as_str(), remote_audience_to_db(&audience)],
17105 |row| row.get(0),
17106 )
17107 .map_err(DbError::from)?;
17108 let remote_object_id = remote_object_id.parse().map_err(|error| {
17109 DbError::Message(format!(
17110 "stored prepared remote object id is invalid: {error}"
17111 ))
17112 })?;
17113 let actual =
17114 PreparedAudiencePackage::from_remote(load_remote_object_on(conn, remote_object_id)?)?;
17115 if actual.package != expected.package
17116 || actual.semantic_bytes != expected.semantic_bytes
17117 || actual.stored_bytes != expected.stored_bytes
17118 || actual.object != expected.object
17119 || actual.remote_object_id != expected.remote_object_id
17120 {
17121 return Err(DbError::Message(format!(
17122 "write {write_id} audience {audience:?} already has different prepared package bytes"
17123 )));
17124 }
17125 Ok(())
17126}
17127
17128fn validate_prepared_blob_on(
17129 conn: &Connection,
17130 write_id: &WriteId,
17131 expected: &PreparedAudienceBlob,
17132) -> Result<(), DbError> {
17133 let locator_hash = expected.blob().locator().locator_hash();
17134 let remote_object_id = expected.remote_object_id();
17135 let (stored_locator_hash, spool_path): (String, Option<String>) = conn
17136 .query_row(
17137 "SELECT locator_hash, spool_path
17138 FROM store_write_blobs
17139 WHERE write_id = ?1 AND audience = ?2 AND remote_object_id = ?3",
17140 rusqlite::params![
17141 write_id.as_str(),
17142 remote_audience_to_db(expected.audience()),
17143 remote_object_id.to_string(),
17144 ],
17145 |row| Ok((row.get(0)?, row.get(1)?)),
17146 )
17147 .map_err(DbError::from)?;
17148 if stored_locator_hash != locator_hash.to_string() {
17149 return Err(DbError::Message(format!(
17150 "write {write_id} audience {:?} exact object {remote_object_id} is indexed under locator {stored_locator_hash}, expected {locator_hash}",
17151 expected.audience()
17152 )));
17153 }
17154 let actual = PreparedAudienceBlob::from_remote(
17155 expected.audience().clone(),
17156 &locator_hash.to_string(),
17157 load_remote_object_on(conn, remote_object_id)?,
17158 spool_path.map(PathBuf::from),
17159 )?;
17160 if actual.blob() != expected.blob()
17161 || actual.spool_path() != expected.spool_path()
17162 || actual.remote_object_id() != expected.remote_object_id()
17163 {
17164 return Err(DbError::Message(format!(
17165 "write {write_id} audience {:?} exact object {remote_object_id} already has different prepared blob bytes",
17166 expected.audience()
17167 )));
17168 }
17169 Ok(())
17170}
17171
17172fn seed_from(hlc: &Hlc, value: Option<String>, context: &str) -> Result<(), DbError> {
17173 if let Some(stamp) = value {
17174 let floor = Timestamp::parse(&stamp)
17175 .ok_or_else(|| DbError::Message(format!("corrupt {context}: {stamp:?}")))?;
17176 hlc.seed(&floor);
17177 }
17178 Ok(())
17179}
17180
17181fn scan_max_updated_at(
17185 conn: &Connection,
17186 synced_tables: &[SyncedTable],
17187 seed_bound_ms: u64,
17188) -> Result<Option<String>, DbError> {
17189 let mut overall: Option<String> = None;
17190 let seed_bound = format!("{seed_bound_ms:013}");
17191 for t in synced_tables {
17192 let sql = format!(
17193 "SELECT MAX(_updated_at) FROM {} WHERE substr(_updated_at, 1, 13) <= ?1",
17194 crate::sync::session::quote_ident(t.name())
17195 );
17196 let value: Option<String> = conn
17197 .query_row(&sql, [&seed_bound], |r| r.get::<_, Option<String>>(0))
17198 .map_err(|e| {
17199 DbError::Message(format!(
17200 "register-floor scan over synced table {}: {e}",
17201 t.name()
17202 ))
17203 })?;
17204 if let Some(v) = value {
17205 overall = Some(match overall {
17206 Some(cur) if cur >= v => cur,
17207 _ => v,
17208 });
17209 }
17210 }
17211 Ok(overall)
17212}
17213
17214fn attach_session<'c>(
17217 conn: &'c Connection,
17218 synced_tables: &[SyncedTable],
17219) -> Result<rusqlite::session::Session<'c>, DbError> {
17220 let mut session = rusqlite::session::Session::new(conn)
17221 .map_err(|e| DbError::Message(format!("failed to create capture session: {e}")))?;
17222 for t in synced_tables {
17223 session.attach(Some(t.name())).map_err(|e| {
17224 DbError::Message(format!(
17225 "failed to attach synced table {} to session: {e}",
17226 t.name()
17227 ))
17228 })?;
17229 }
17230 Ok(session)
17231}
17232
17233fn capture_changeset(session: &mut rusqlite::session::Session<'_>) -> Result<Vec<u8>, DbError> {
17237 let mut buf = Vec::new();
17238 session
17239 .changeset_strm(&mut buf)
17240 .map(|()| buf)
17241 .map_err(DbError::from)
17242}
17243
17244fn open_connection(path: &Path) -> Result<Connection, DbError> {
17245 let conn = Connection::open(path).map_err(DbError::from)?;
17246 conn.query_row("PRAGMA journal_mode = WAL", [], |_| Ok(()))
17250 .map_err(DbError::from)?;
17251 Ok(conn)
17252}
17253
17254fn open_connection_read_only(path: &Path) -> Result<Connection, DbError> {
17259 use rusqlite::OpenFlags;
17260 let flags = OpenFlags::SQLITE_OPEN_READ_ONLY
17261 | OpenFlags::SQLITE_OPEN_NO_MUTEX
17262 | OpenFlags::SQLITE_OPEN_URI;
17263 Connection::open_with_flags(path, flags).map_err(DbError::from)
17264}
17265
17266#[cfg(test)]
17267mod tests {
17268 use super::*;
17269 use crate::blob::BLOB_TOMBSTONE_GRACE;
17270
17271 fn notes_migration() -> Migration {
17272 Migration::sql(
17273 1,
17274 "notes",
17275 "CREATE TABLE notes (
17276 id TEXT PRIMARY KEY,
17277 body TEXT NOT NULL,
17278 _updated_at TEXT NOT NULL
17279 ) STRICT;",
17280 )
17281 }
17282
17283 fn things_migration() -> Migration {
17284 Migration::sql(
17285 1,
17286 "things",
17287 "CREATE TABLE things (
17288 id TEXT PRIMARY KEY,
17289 body TEXT NOT NULL,
17290 _updated_at TEXT NOT NULL
17291 ) STRICT;",
17292 )
17293 }
17294
17295 fn things_table(identity: crate::sync::session::RowIdentity) -> SyncedTable {
17296 SyncedTable::new("things", identity)
17297 }
17298
17299 fn scoped_things_migration() -> Migration {
17300 Migration::sql(
17301 1,
17302 "scoped things",
17303 "CREATE TABLE things (
17304 id TEXT PRIMARY KEY,
17305 audience TEXT,
17306 body TEXT NOT NULL,
17307 _updated_at TEXT NOT NULL
17308 ) STRICT;",
17309 )
17310 }
17311
17312 fn scoped_things_table() -> SyncedTable {
17313 SyncedTable::new("things", crate::sync::session::RowIdentity::SharedKey)
17314 .scoped_by("audience")
17315 }
17316
17317 fn exact_blob_binding(row_id: &str, stamp: &str, bytes: &[u8]) -> RowBlobLocatorBinding {
17318 let plaintext_hash = ObjectHash::digest(bytes);
17319 let uploader_bytes = b"database test uploader registration";
17320 let uploader = crate::sync::store_commit::StoreDeviceRegistrationRef {
17321 device_id: "aa".repeat(32).parse().expect("valid test device id"),
17322 registration_hash: ObjectHash::digest(uploader_bytes),
17323 object: ExactObjectRef::new(
17324 crate::storage::cloud::ObjectSlot::logical(
17325 "store-v1/devices/database-test-uploader.json".to_string(),
17326 )
17327 .expect("valid uploader registration slot"),
17328 uploader_bytes.len() as u64,
17329 ObjectHash::digest(uploader_bytes),
17330 ),
17331 };
17332 let locator = BlobLocator::browsable(
17333 "images",
17334 row_id,
17335 uploader,
17336 format!("photos/{row_id}.bin"),
17337 bytes.len() as u64,
17338 plaintext_hash,
17339 )
17340 .expect("valid locator");
17341 let stored = b"stored representation".to_vec();
17342 let slot = crate::storage::cloud::ObjectSlot::logical(locator.semantic_key())
17343 .expect("valid exact slot");
17344 let object = ExactObjectRef::new(slot, stored.len() as u64, ObjectHash::digest(&stored));
17345 RowBlobLocatorBinding::new(
17346 "photos",
17347 row_id,
17348 stamp,
17349 "id",
17350 StoredBlobRef::new(locator, object).expect("valid stored blob"),
17351 )
17352 .expect("valid row binding")
17353 }
17354
17355 #[test]
17356 fn snapshot_blob_owner_rejects_other_activation_and_sequence() {
17357 let conn = Connection::open_in_memory().expect("open snapshot owner database");
17358 apply_coven_schema(&conn).expect("apply snapshot owner schema");
17359 let binding = exact_blob_binding(
17360 "snapshot-owner-photo",
17361 "0000000001000-0000-owner",
17362 b"snapshot owner bytes",
17363 );
17364 let expected = crate::sync::remote_object::SnapshotObjectOwner {
17365 activation: StreamActivationId::from_hash(ObjectHash::digest(
17366 b"verified snapshot activation",
17367 )),
17368 sequence: 7,
17369 };
17370 let install = |owner: crate::sync::remote_object::SnapshotObjectOwner| {
17371 conn.execute("DELETE FROM remote_objects", [])
17372 .expect("clear snapshot owner");
17373 let remote = RemoteObjectRecord::snapshot_activated_blob(binding.blob(), owner)
17374 .expect("build snapshot-owned blob");
17375 conn.execute(
17376 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
17377 rusqlite::params![
17378 remote.object_id().to_string(),
17379 serde_json::to_string(&remote).expect("serialize snapshot-owned blob"),
17380 ],
17381 )
17382 .expect("install snapshot-owned blob");
17383 };
17384
17385 install(expected.clone());
17386 validate_snapshot_object_owner_records_on(&conn, &expected)
17387 .expect("verified snapshot owner matches");
17388
17389 install(crate::sync::remote_object::SnapshotObjectOwner {
17390 activation: StreamActivationId::from_hash(ObjectHash::digest(
17391 b"other snapshot activation",
17392 )),
17393 sequence: expected.sequence,
17394 });
17395 assert!(validate_snapshot_object_owner_records_on(&conn, &expected).is_err());
17396
17397 install(crate::sync::remote_object::SnapshotObjectOwner {
17398 activation: expected.activation,
17399 sequence: expected.sequence + 1,
17400 });
17401 assert!(validate_snapshot_object_owner_records_on(&conn, &expected).is_err());
17402 }
17403
17404 fn local_row_blob(row_id: &str, stamp: &str, bytes: &[u8]) -> RowBlobRef {
17405 let binding = exact_blob_binding(row_id, stamp, bytes);
17406 let locator = binding.blob().locator();
17407 RowBlobRef::new(
17408 "photos".to_string(),
17409 row_id.to_string(),
17410 stamp.to_string(),
17411 "id".to_string(),
17412 BlobRef {
17413 namespace: locator.namespace().to_string(),
17414 id: locator.blob_id().to_string(),
17415 scope: crate::blob::BlobScope::Master,
17416 cloud_path: locator.cloud_path().map(str::to_string),
17417 provenance: Provenance::HostProvided,
17418 fill: crate::blob::CacheFill::CacheLazy,
17419 },
17420 locator.plaintext_size(),
17421 locator.plaintext_hash(),
17422 RowBlobAuthority::Local,
17423 None,
17424 )
17425 .expect("valid Local row blob")
17426 }
17427
17428 fn open_outbox_database(device_id: &str) -> Database {
17429 Database::open(
17430 Path::new(":memory:"),
17431 Vec::new(),
17432 BLOB_TOMBSTONE_GRACE,
17433 crate::blob::TransferLimits::serial(),
17434 crate::WritePolicy::MergeConcurrent,
17435 device_id.to_string(),
17436 &[],
17437 )
17438 .expect("open outbox database")
17439 .0
17440 }
17441
17442 fn test_candidate_family() -> crate::sync::store_commit::CandidateFamilyId {
17443 crate::sync::store_commit::CandidateFamilyId::from_hash(ObjectHash::digest(
17444 b"database test candidate family",
17445 ))
17446 }
17447
17448 fn test_commit_coord() -> StoreCommitCoord {
17449 StoreCommitCoord::MergeConcurrent {
17450 stream_id: crate::sync::membership::AuthorStreamId::from_bytes([7; 32]),
17451 sequence: 1,
17452 }
17453 }
17454
17455 fn test_commit_ref() -> StoreBatchCommitRef {
17456 let coord = test_commit_coord();
17457 let commit_hash = ObjectHash::digest(b"database test commit");
17458 let StoreCommitCoord::MergeConcurrent { stream_id, .. } = &coord else {
17459 unreachable!("database test coordinate is MergeConcurrent")
17460 };
17461 let slot = crate::storage::cloud::ObjectSlot::logical(format!(
17462 "{}.json",
17463 commit_semantic_prefix(
17464 test_candidate_family(),
17465 &stream_id.to_string(),
17466 coord.sequence(),
17467 commit_hash,
17468 )
17469 ))
17470 .expect("valid database test commit slot");
17471 StoreBatchCommitRef {
17472 coord,
17473 commit_hash,
17474 object: ExactObjectRef::new(slot, 1, ObjectHash::digest(b"x")),
17475 }
17476 }
17477
17478 #[tokio::test]
17479 async fn upload_retry_preserves_prepared_object_handoff() {
17480 let db = open_outbox_database("prepared-upload-retry");
17481 let row = local_row_blob("photo", "0000000001000-0000-a", b"photo bytes");
17482 let initial_row = row.clone();
17483 db.call(move |conn| {
17484 Database::enqueue_upload_on(
17485 conn,
17486 "photos",
17487 "photo",
17488 &initial_row,
17489 Path::new("/source/first"),
17490 false,
17491 "2026-07-16T10:00:00Z",
17492 )
17493 })
17494 .await
17495 .expect("enqueue upload");
17496
17497 let entry = db
17498 .get_pending_cloud_uploads()
17499 .await
17500 .expect("read upload")
17501 .pop()
17502 .expect("upload entry");
17503 let stored = exact_blob_binding("photo", "0000000001000-0000-a", b"photo bytes")
17504 .blob()
17505 .clone();
17506 let spool_path = PathBuf::from("/spool/prepared");
17507 db.mark_cloud_upload_prepared(
17508 &entry,
17509 crate::sync::audience_package::PackageAudience::Store,
17510 stored.clone(),
17511 spool_path.clone(),
17512 )
17513 .await
17514 .expect("record prepared object");
17515
17516 db.call(move |conn| {
17517 Database::enqueue_upload_on(
17518 conn,
17519 "photos",
17520 "photo",
17521 &row,
17522 Path::new("/source/retried"),
17523 true,
17524 "2026-07-16T10:01:00Z",
17525 )
17526 })
17527 .await
17528 .expect("retry upload command");
17529
17530 let entries = db
17531 .get_pending_cloud_uploads()
17532 .await
17533 .expect("read retried upload");
17534 assert_eq!(entries.len(), 1);
17535 assert_eq!(
17536 entries[0].operation,
17537 OutboxOperation::Upload {
17538 root_table: "photos".to_string(),
17539 root_id: "photo".to_string(),
17540 row: local_row_blob("photo", "0000000001000-0000-a", b"photo bytes"),
17541 source_path: PathBuf::from("/source/retried"),
17542 retain_pinned: true,
17543 state: OutboxUploadState::Prepared {
17544 authority: crate::sync::audience_package::PackageAudience::Store,
17545 stored,
17546 spool_path,
17547 },
17548 }
17549 );
17550 db.mark_cloud_upload_created(&entries[0])
17551 .await
17552 .expect("record exact cloud creation");
17553 let created = db
17554 .get_pending_cloud_uploads()
17555 .await
17556 .expect("read Created upload");
17557 let OutboxOperation::Upload { state, .. } = &created[0].operation else {
17558 panic!("upload query returned a non-upload operation");
17559 };
17560 assert!(matches!(
17561 state,
17562 OutboxUploadState::Created {
17563 authority: crate::sync::audience_package::PackageAudience::Store,
17564 ..
17565 }
17566 ));
17567 }
17568
17569 #[tokio::test]
17570 async fn repeated_exact_delete_resets_retry_state_without_duplication() {
17571 let db = open_outbox_database("repeat-delete");
17572 let stored = exact_blob_binding("photo", "0000000001000-0000-a", b"photo bytes")
17573 .blob()
17574 .clone();
17575 let delete_stored = stored.clone();
17576 db.call(move |conn| {
17577 Database::enqueue_delete_on(conn, &delete_stored, "2026-07-16T10:00:00Z")
17578 })
17579 .await
17580 .expect("enqueue delete");
17581 let failed = db
17582 .get_pending_cloud_deletes()
17583 .await
17584 .expect("read pending delete")
17585 .pop()
17586 .expect("delete entry");
17587 db.record_cloud_outbox_failure(&failed, "provider unavailable", "2026-07-16T10:00:30Z")
17588 .await
17589 .expect("record delete failure");
17590 let retry_stored = stored.clone();
17591 db.call(move |conn| {
17592 Database::enqueue_delete_on(conn, &retry_stored, "2026-07-16T10:01:00Z")
17593 })
17594 .await
17595 .expect("repeat exact delete");
17596
17597 let deletes = db.get_pending_cloud_deletes().await.expect("read deletes");
17598 assert_eq!(deletes.len(), 1);
17599 assert_eq!(deletes[0].attempt_count, 0);
17600 assert_eq!(deletes[0].last_attempt_at, None);
17601 assert_eq!(deletes[0].operation, OutboxOperation::Delete { stored });
17602 }
17603
17604 #[tokio::test]
17605 async fn exact_deletes_for_distinct_objects_remain_distinct() {
17606 let db = open_outbox_database("distinct-deletes");
17607 let first = exact_blob_binding("photo-a", "0000000001000-0000-a", b"photo a")
17608 .blob()
17609 .clone();
17610 let second = exact_blob_binding("photo-b", "0000000001000-0000-a", b"photo b")
17611 .blob()
17612 .clone();
17613 db.call(move |conn| Database::enqueue_delete_on(conn, &first, "2026-07-16T10:00:00Z"))
17614 .await
17615 .expect("enqueue first delete");
17616 db.call(move |conn| Database::enqueue_delete_on(conn, &second, "2026-07-16T10:01:00Z"))
17617 .await
17618 .expect("enqueue second delete");
17619
17620 let deletes = db.get_pending_cloud_deletes().await.expect("read deletes");
17621 assert_eq!(deletes.len(), 2);
17622 assert_ne!(deletes[0].operation, deletes[1].operation);
17623 }
17624
17625 fn blob_binding_table() -> SyncedTable {
17626 SyncedTable::new("photos", crate::sync::session::RowIdentity::SharedKey).carries_blob(
17627 crate::sync::session::BlobDecl::new(
17628 "images",
17629 Provenance::HostProvided,
17630 crate::blob::CacheFill::CacheLazy,
17631 )
17632 .with_cloud_path_column("cloud_path"),
17633 )
17634 }
17635
17636 fn insert_blob_row(conn: &Connection, row_id: &str, stamp: &str, bytes: &[u8]) {
17637 conn.execute(
17638 "INSERT INTO photos (id, size, hash, cloud_path, _updated_at)
17639 VALUES (?1, ?2, ?3, ?4, ?5)",
17640 rusqlite::params![
17641 row_id,
17642 bytes.len() as i64,
17643 ObjectHash::digest(bytes).to_string(),
17644 format!("photos/{row_id}.bin"),
17645 stamp,
17646 ],
17647 )
17648 .expect("insert blob row");
17649 }
17650
17651 #[test]
17652 fn blob_bindings_install_only_for_exact_winning_row_stamps() {
17653 let mut conn = Connection::open_in_memory().expect("open");
17654 apply_coven_schema(&conn).expect("apply coven schema");
17655 conn.execute_batch(
17656 "CREATE TABLE photos (
17657 id TEXT PRIMARY KEY,
17658 size INTEGER NOT NULL,
17659 hash TEXT NOT NULL,
17660 cloud_path TEXT NOT NULL,
17661 _updated_at TEXT NOT NULL
17662 ) STRICT;",
17663 )
17664 .expect("create photos");
17665 let table = blob_binding_table();
17666 let tables = vec![table];
17667 let gates = Gates::from_tables(&conn, &tables).expect("build gates");
17668 insert_blob_row(&conn, "winner", "0000000001000-0000-a", b"winner bytes");
17669 insert_blob_row(&conn, "loser", "0000000002000-0000-b", b"loser bytes");
17670
17671 let package = AudiencePackage::store(
17672 ObjectHash::digest(b"root"),
17673 test_candidate_family(),
17674 WriteId::from_generated("write-1".to_string()),
17675 test_commit_coord(),
17676 1,
17677 Vec::new(),
17678 vec![
17679 exact_blob_binding("winner", "0000000001000-0000-a", b"winner bytes"),
17680 exact_blob_binding("loser", "0000000001000-0000-b", b"loser bytes"),
17681 ],
17682 )
17683 .expect("build package");
17684 let activation = BlobActivation {
17685 coord: test_commit_coord(),
17686 };
17687
17688 let tx = conn.transaction().expect("begin");
17689 Database::install_pulled_blob_activations_on(&tx, &package, &test_commit_ref())
17690 .expect("install pulled blob activation");
17691 let winning_rows = [crate::sync::apply::WinningRow {
17692 table: "photos".to_string(),
17693 row_id: "winner".to_string(),
17694 row_stamp: Some("0000000001000-0000-a".to_string()),
17695 }];
17696 assert_eq!(
17697 Database::install_winning_blob_bindings_on(
17698 &tx,
17699 &gates,
17700 &tables,
17701 &package,
17702 &activation,
17703 &winning_rows,
17704 )
17705 .expect("install winning binding"),
17706 1
17707 );
17708 tx.commit().expect("commit");
17709
17710 assert_eq!(
17711 conn.query_row("SELECT count(*) FROM blob_locators", [], |row| row
17712 .get::<_, i64>(0))
17713 .expect("count locators"),
17714 1
17715 );
17716 assert_eq!(
17717 conn.query_row("SELECT row_id FROM row_blob_locators", [], |row| row
17718 .get::<_, String>(0))
17719 .expect("read binding"),
17720 "winner"
17721 );
17722 let resolved = Database::row_blob_ref_on(&conn, &gates, &tables[0], "winner")
17723 .expect("resolve exact row blob reference");
17724 assert_eq!(resolved.row_stamp(), "0000000001000-0000-a");
17725 assert_eq!(
17726 resolved.plaintext_hash(),
17727 ObjectHash::digest(b"winner bytes")
17728 );
17729 assert_eq!(
17730 resolved
17731 .stored()
17732 .expect("remote row carries exact locator")
17733 .locator()
17734 .blob_id(),
17735 "winner"
17736 );
17737 }
17738
17739 #[test]
17740 fn mismatched_blob_values_roll_back_locator_installation_with_rows() {
17741 let mut conn = Connection::open_in_memory().expect("open");
17742 apply_coven_schema(&conn).expect("apply coven schema");
17743 conn.execute_batch(
17744 "CREATE TABLE photos (
17745 id TEXT PRIMARY KEY,
17746 size INTEGER NOT NULL,
17747 hash TEXT NOT NULL,
17748 cloud_path TEXT NOT NULL,
17749 _updated_at TEXT NOT NULL
17750 ) STRICT;",
17751 )
17752 .expect("create photos");
17753 let tables = vec![blob_binding_table()];
17754 let gates = Gates::from_tables(&conn, &tables).expect("build gates");
17755 insert_blob_row(&conn, "photo", "0000000001000-0000-a", b"actual bytes");
17756 let package = AudiencePackage::store(
17757 ObjectHash::digest(b"root"),
17758 test_candidate_family(),
17759 WriteId::from_generated("write-1".to_string()),
17760 test_commit_coord(),
17761 1,
17762 Vec::new(),
17763 vec![exact_blob_binding(
17764 "photo",
17765 "0000000001000-0000-a",
17766 b"different bytes",
17767 )],
17768 )
17769 .expect("build package");
17770 let activation = BlobActivation {
17771 coord: test_commit_coord(),
17772 };
17773
17774 let tx = conn.transaction().expect("begin");
17775 Database::install_pulled_blob_activations_on(&tx, &package, &test_commit_ref())
17776 .expect("install pulled blob activation");
17777 let winning_rows = [crate::sync::apply::WinningRow {
17778 table: "photos".to_string(),
17779 row_id: "photo".to_string(),
17780 row_stamp: Some("0000000001000-0000-a".to_string()),
17781 }];
17782 let error = Database::install_winning_blob_bindings_on(
17783 &tx,
17784 &gates,
17785 &tables,
17786 &package,
17787 &activation,
17788 &winning_rows,
17789 )
17790 .expect_err("mismatched locator must fail");
17791 assert!(error
17792 .to_string()
17793 .contains("does not match winning row values"));
17794 tx.rollback().expect("roll back");
17795 assert_eq!(
17796 conn.query_row("SELECT count(*) FROM blob_locators", [], |row| row
17797 .get::<_, i64>(0))
17798 .expect("count locators"),
17799 0
17800 );
17801 }
17802
17803 fn assert_coven_schema_mutation_is_rejected(
17804 name: &str,
17805 tables: Vec<SyncedTable>,
17806 write_policy: crate::WritePolicy,
17807 migrations: Vec<Migration>,
17808 mutate: impl FnOnce(&Connection),
17809 ) {
17810 let directory = tempfile::tempdir().expect("temp dir");
17811 let path = directory.path().join(format!("{name}.sqlite"));
17812 let (database, _) = Database::open(
17813 &path,
17814 tables.clone(),
17815 BLOB_TOMBSTONE_GRACE,
17816 crate::blob::TransferLimits::serial(),
17817 write_policy,
17818 "schema-seed".to_string(),
17819 &migrations,
17820 )
17821 .expect("seed database");
17822 drop(database);
17823
17824 let conn = Connection::open(&path).expect("open database for schema mutation");
17825 mutate(&conn);
17826 drop(conn);
17827
17828 let writer_error = match Database::open(
17829 &path,
17830 tables.clone(),
17831 BLOB_TOMBSTONE_GRACE,
17832 crate::blob::TransferLimits::serial(),
17833 write_policy,
17834 "schema-writer".to_string(),
17835 &migrations,
17836 ) {
17837 Ok(_) => panic!("writer must reject Coven schema mutation {name}"),
17838 Err(error) => error.to_string(),
17839 };
17840 assert!(
17841 writer_error.contains("Coven schema"),
17842 "{name}: {writer_error}"
17843 );
17844
17845 let reader_error = match Database::open_read_only(
17846 &path,
17847 tables,
17848 BLOB_TOMBSTONE_GRACE,
17849 crate::blob::TransferLimits::serial(),
17850 write_policy,
17851 "schema-reader".to_string(),
17852 &migrations,
17853 ) {
17854 Ok(_) => panic!("read-only open must reject Coven schema mutation {name}"),
17855 Err(error) => error.to_string(),
17856 };
17857 assert!(
17858 reader_error.contains("Coven schema"),
17859 "{name}: {reader_error}"
17860 );
17861
17862 let conn = Connection::open(&path).expect("inspect rejected database");
17863 let host_device_id: String = conn
17864 .query_row(
17865 "SELECT value FROM protocol_state WHERE key = ?1",
17866 [HOST_DEVICE_ID_STATE_KEY],
17867 |row| row.get(0),
17868 )
17869 .expect("host device id");
17870 assert_eq!(host_device_id, "schema-seed", "{name} was rewritten");
17871 }
17872
17873 #[tokio::test]
17874 async fn required_store_root_hash_rejects_missing_and_malformed_exact_authority() {
17875 let (db, _) = Database::open(
17876 Path::new(":memory:"),
17877 vec![SyncedTable::new(
17878 "notes",
17879 crate::sync::session::RowIdentity::SharedKey,
17880 )],
17881 BLOB_TOMBSTONE_GRACE,
17882 crate::blob::TransferLimits::serial(),
17883 crate::WritePolicy::MergeConcurrent,
17884 "required-store-root".to_string(),
17885 &[notes_migration()],
17886 )
17887 .expect("open database");
17888
17889 let missing = db
17890 .required_store_root_hash()
17891 .await
17892 .expect_err("missing Store root must fail");
17893 assert!(matches!(missing, DbError::StoreRootHashMissing));
17894
17895 db.call(|conn| {
17896 conn.execute(
17897 "INSERT INTO store_protocol_root_authority
17898 (singleton, store_root_hash, store_protocol_root_bytes, store_root_object)
17899 VALUES (1, ?1, X'00', '{}')",
17900 ["00".repeat(32)],
17901 )
17902 .map(|_| ())
17903 .map_err(DbError::from)
17904 })
17905 .await
17906 .expect("write malformed exact Store root authority");
17907 let malformed = db
17908 .required_store_root_hash()
17909 .await
17910 .expect_err("malformed Store root must fail");
17911 assert!(matches!(
17912 malformed,
17913 DbError::Message(reason) if !reason.is_empty()
17914 ));
17915
17916 db.call(|conn| {
17917 conn.execute("DELETE FROM store_protocol_root_authority", [])
17918 .map(|_| ())
17919 .map_err(DbError::from)
17920 })
17921 .await
17922 .expect("remove malformed authority");
17923 let signer = crate::keys::UserKeypair::generate();
17924 let founder_provider_admin =
17925 crate::sync::test_helpers::test_founder_provider_admin("required-store-root");
17926 let descriptor = crate::sync::store_commit::StoreCreationDescriptor {
17927 version: crate::sync::store_commit::STORE_PROTOCOL_VERSION,
17928 creation_id: crate::sync::store_commit::StoreCreationId::from_nonce(
17929 "required-store-root",
17930 ),
17931 provider: crate::sync::storage::StoreProviderBinding::S3 {
17932 endpoint: crate::sync::storage::S3EndpointBinding::Custom {
17933 origin: "https://test.invalid".to_string(),
17934 },
17935 region: "test-region".to_string(),
17936 bucket: "required-store-root-bucket".to_string(),
17937 key_prefix: None,
17938 },
17939 schema_version: db.schema_version(),
17940 sync_routing_hash: db.sync_routing_hash(),
17941 write_policy: crate::WritePolicy::MergeConcurrent,
17942 founder_pubkey: crate::keys::public_key_hex(&signer),
17943 founder_grant: crate::sync::test_helpers::test_membership_grant_id(
17944 "required-store-root founder",
17945 ),
17946 root_slot: crate::storage::cloud::ObjectSlot::logical(
17947 crate::sync::store_commit::STORE_PROTOCOL_ROOT_LOGICAL_KEY.to_string(),
17948 )
17949 .expect("valid Store root slot"),
17950 founder_registration: crate::storage::cloud::ObjectSlot::logical(
17951 "store-v1/test/required-store-root/registration.json".to_string(),
17952 )
17953 .expect("valid founder registration slot"),
17954 founder_provider_admin,
17955 membership: crate::sync::store_commit::StoreMembershipGenesis::MergeConcurrent {
17956 founder_membership: crate::sync::store_commit::GrantStreamAnchor::StoreMembership {
17957 first_slot: crate::storage::cloud::ObjectSlot::logical(
17958 "store-v1/test/required-store-root/membership/1.json".to_string(),
17959 )
17960 .expect("valid membership slot"),
17961 },
17962 },
17963 founder_recovery: crate::sync::store_commit::GrantStreamAnchor::OwnerRecovery {
17964 first_slot: crate::storage::cloud::ObjectSlot::logical(
17965 "store-v1/test/required-store-root/recovery/1.json".to_string(),
17966 )
17967 .expect("valid recovery slot"),
17968 },
17969 };
17970 let root = crate::sync::store_commit::StoreProtocolRoot::signed(descriptor, &signer)
17971 .expect("sign Store root authority");
17972 let bytes = root.to_bytes();
17973 let expected = root.object_hash();
17974 let reference = crate::sync::store_commit::StoreRootRef {
17975 store_root_id: root.descriptor.store_root_id(),
17976 store_root_hash: expected,
17977 object: ExactObjectRef::new(
17978 crate::storage::cloud::ObjectSlot::logical(
17979 "store-v1/store-protocol-root/required-store-root.json".to_string(),
17980 )
17981 .expect("valid Store root slot"),
17982 bytes.len() as u64,
17983 ObjectHash::digest(&bytes),
17984 ),
17985 };
17986 db.install_store_root_authority(reference, bytes)
17987 .await
17988 .expect("install exact Store root authority");
17989 assert_eq!(db.required_store_root_hash().await.unwrap(), expected);
17990 }
17991
17992 #[test]
17993 fn fresh_open_rolls_back_host_schema_and_coven_metadata_when_routing_is_invalid() {
17994 let directory = tempfile::tempdir().expect("temp dir");
17995 let path = directory.path().join("fresh-routing-failure.sqlite");
17996 let result = Database::open(
17997 &path,
17998 vec![things_table(crate::sync::session::RowIdentity::SharedKey)],
17999 BLOB_TOMBSTONE_GRACE,
18000 crate::blob::TransferLimits::serial(),
18001 crate::WritePolicy::MergeConcurrent,
18002 "fresh-routing-failure".to_string(),
18003 &[Migration::sql(
18004 1,
18005 "invalid routing",
18006 "CREATE TABLE local_parents (id TEXT PRIMARY KEY) STRICT;
18007 CREATE TABLE things (
18008 id TEXT PRIMARY KEY,
18009 local_parent_id TEXT NOT NULL REFERENCES local_parents(id),
18010 _updated_at TEXT NOT NULL
18011 ) STRICT;",
18012 )],
18013 );
18014 let error = match result {
18015 Ok(_) => panic!("fresh open must reject a synced-to-local foreign key"),
18016 Err(error) => error.to_string(),
18017 };
18018 assert!(error.contains("local_parents"), "{error}");
18019
18020 let conn = Connection::open(&path).expect("inspect rolled-back database");
18021 let user_version: i64 = conn
18022 .query_row("PRAGMA user_version", [], |row| row.get(0))
18023 .expect("user version");
18024 let durable_tables: i64 = conn
18025 .query_row(
18026 "SELECT COUNT(*) FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
18027 [],
18028 |row| row.get(0),
18029 )
18030 .expect("durable tables");
18031 assert_eq!(user_version, 0);
18032 assert_eq!(durable_tables, 0);
18033 }
18034
18035 #[test]
18036 fn initialized_open_commits_ordinary_migration_without_changing_routing_contract() {
18037 let directory = tempfile::tempdir().expect("temp dir");
18038 let path = directory.path().join("ordinary-migration.sqlite");
18039 let migrations = [things_migration()];
18040 let (database, _) = Database::open(
18041 &path,
18042 vec![things_table(crate::sync::session::RowIdentity::SharedKey)],
18043 BLOB_TOMBSTONE_GRACE,
18044 crate::blob::TransferLimits::serial(),
18045 crate::WritePolicy::MergeConcurrent,
18046 "ordinary-first-open".to_string(),
18047 &migrations,
18048 )
18049 .expect("initial open");
18050 let pinned_hash = database.sync_routing_hash();
18051 drop(database);
18052
18053 let migrations = [
18054 things_migration(),
18055 Migration::sql(
18056 2,
18057 "ordinary column and index",
18058 "ALTER TABLE things ADD COLUMN ordinary TEXT DEFAULT 'ordinary';
18059 CREATE INDEX things_ordinary ON things(ordinary);",
18060 ),
18061 ];
18062 let (database, _) = Database::open(
18063 &path,
18064 vec![things_table(crate::sync::session::RowIdentity::SharedKey)],
18065 BLOB_TOMBSTONE_GRACE,
18066 crate::blob::TransferLimits::serial(),
18067 crate::WritePolicy::MergeConcurrent,
18068 "ordinary-first-open".to_string(),
18069 &migrations,
18070 )
18071 .expect("ordinary migration open");
18072 assert_eq!(database.schema_version(), 2);
18073 assert_eq!(database.sync_routing_hash(), pinned_hash);
18074 drop(database);
18075
18076 let conn = Connection::open(&path).expect("inspect migrated database");
18077 let ordinary_ordinal: i64 = conn
18078 .query_row(
18079 "SELECT cid FROM pragma_table_info('things') WHERE name = 'ordinary'",
18080 [],
18081 |row| row.get(0),
18082 )
18083 .expect("ordinary column");
18084 assert_eq!(ordinary_ordinal, 3);
18085 }
18086
18087 #[test]
18088 fn initialized_open_rolls_back_routing_migration_and_user_version() {
18089 let directory = tempfile::tempdir().expect("temp dir");
18090 let path = directory.path().join("routing-migration.sqlite");
18091 let v1 = || {
18092 Migration::sql(
18093 1,
18094 "gated things",
18095 "CREATE TABLE things (
18096 id TEXT PRIMARY KEY,
18097 audience TEXT COLLATE BINARY NOT NULL,
18098 _updated_at TEXT NOT NULL
18099 ) STRICT;",
18100 )
18101 };
18102 let table = || {
18103 SyncedTable::new("things", crate::sync::session::RowIdentity::SharedKey)
18104 .gated_by("audience")
18105 };
18106 let (database, _) = Database::open(
18107 &path,
18108 vec![table()],
18109 BLOB_TOMBSTONE_GRACE,
18110 crate::blob::TransferLimits::serial(),
18111 crate::WritePolicy::MergeConcurrent,
18112 "routing-first-open".to_string(),
18113 &[v1()],
18114 )
18115 .expect("initial open");
18116 drop(database);
18117
18118 let v2 = Migration::sql(
18119 2,
18120 "change audience collation",
18121 "CREATE TABLE things_next (
18122 id TEXT PRIMARY KEY,
18123 audience TEXT COLLATE NOCASE NOT NULL,
18124 _updated_at TEXT NOT NULL
18125 ) STRICT;
18126 INSERT INTO things_next SELECT * FROM things;
18127 DROP TABLE things;
18128 ALTER TABLE things_next RENAME TO things;",
18129 );
18130 let result = Database::open(
18131 &path,
18132 vec![table()],
18133 BLOB_TOMBSTONE_GRACE,
18134 crate::blob::TransferLimits::serial(),
18135 crate::WritePolicy::MergeConcurrent,
18136 "routing-first-open".to_string(),
18137 &[v1(), v2],
18138 );
18139 let error = match result {
18140 Ok(_) => panic!("routing migration must not commit"),
18141 Err(error) => error.to_string(),
18142 };
18143 assert!(error.contains("sync-routing hash"), "{error}");
18144
18145 let conn = Connection::open(&path).expect("inspect rolled-back migration");
18146 let user_version: i64 = conn
18147 .query_row("PRAGMA user_version", [], |row| row.get(0))
18148 .expect("user version");
18149 let things_next: i64 = conn
18150 .query_row(
18151 "SELECT COUNT(*) FROM sqlite_schema WHERE type = 'table' AND name = 'things_next'",
18152 [],
18153 |row| row.get(0),
18154 )
18155 .expect("things_next presence");
18156 let (_, collation, _, _, _) = conn
18157 .column_metadata(None::<&str>, "things", "audience")
18158 .expect("audience metadata");
18159 assert_eq!(user_version, 1);
18160 assert_eq!(things_next, 0);
18161 assert_eq!(collation.unwrap().to_bytes(), b"BINARY");
18162 }
18163
18164 #[test]
18165 fn writer_and_read_only_open_reject_every_coven_schema_shape_change_without_rewriting() {
18166 let cases = [
18167 ("missing-table", "DROP TABLE published_store_acks;"),
18168 (
18169 "changed-primary-key-and-constraints",
18170 "DROP TABLE published_store_acks;
18171 CREATE TABLE published_store_acks (
18172 revision INTEGER,
18173 ack_hash TEXT NOT NULL,
18174 PRIMARY KEY (ack_hash)
18175 ) STRICT;",
18176 ),
18177 (
18178 "unexpected-index",
18179 "CREATE INDEX coven_unexpected_store_write_status ON store_writes(status);",
18180 ),
18181 (
18182 "missing-strict",
18183 "DROP TABLE local_cleanup_intents;
18184 CREATE TABLE local_cleanup_intents (
18185 namespace TEXT NOT NULL,
18186 blob_id TEXT NOT NULL,
18187 PRIMARY KEY (namespace, blob_id)
18188 );",
18189 ),
18190 (
18191 "missing-without-rowid",
18192 "DROP TABLE _coven_audience;
18193 CREATE TABLE _coven_audience (
18194 routing_id TEXT PRIMARY KEY,
18195 circle_id TEXT,
18196 _updated_at TEXT NOT NULL
18197 ) STRICT;",
18198 ),
18199 ];
18200
18201 for (name, mutation) in cases {
18202 assert_coven_schema_mutation_is_rejected(
18203 name,
18204 vec![scoped_things_table()],
18205 crate::WritePolicy::MergeConcurrent,
18206 vec![scoped_things_migration()],
18207 |conn| {
18208 conn.execute_batch(mutation).expect("mutate Coven schema");
18209 },
18210 );
18211 }
18212 }
18213
18214 #[test]
18215 fn writer_and_read_only_open_reject_unexpected_policy_specific_coven_tables() {
18216 assert_coven_schema_mutation_is_rejected(
18217 "unexpected-routing",
18218 vec![things_table(crate::sync::session::RowIdentity::SharedKey)],
18219 crate::WritePolicy::Serial,
18220 vec![things_migration()],
18221 |conn| {
18222 crate::db::apply_coven_routing_schema(conn)
18223 .expect("add inapplicable routing tables");
18224 },
18225 );
18226 }
18227
18228 #[test]
18229 fn first_open_rolls_back_host_migration_when_gate_model_is_invalid() {
18230 let directory = tempfile::tempdir().expect("temp dir");
18231 let path = directory.path().join("invalid-gate-migration.sqlite");
18232 let migration = Migration::sql(
18233 1,
18234 "composite gate relation",
18235 "CREATE TABLE parents (
18236 id TEXT PRIMARY KEY,
18237 code TEXT NOT NULL,
18238 shared INTEGER NOT NULL,
18239 _updated_at TEXT NOT NULL,
18240 UNIQUE (id, code)
18241 ) STRICT;
18242 CREATE TABLE children (
18243 id TEXT PRIMARY KEY,
18244 parent_id TEXT NOT NULL,
18245 parent_code TEXT NOT NULL,
18246 _updated_at TEXT NOT NULL,
18247 FOREIGN KEY (parent_id, parent_code) REFERENCES parents(id, code)
18248 ) STRICT;",
18249 );
18250 let tables = vec![
18251 SyncedTable::new("parents", crate::sync::session::RowIdentity::SharedKey)
18252 .gated_by("shared"),
18253 SyncedTable::new("children", crate::sync::session::RowIdentity::SharedKey),
18254 ];
18255
18256 let error = match Database::open(
18257 &path,
18258 tables,
18259 BLOB_TOMBSTONE_GRACE,
18260 crate::blob::TransferLimits::serial(),
18261 crate::WritePolicy::MergeConcurrent,
18262 "invalid-gate-open".to_string(),
18263 &[migration],
18264 ) {
18265 Ok(_) => panic!("an invalid gate model must reject the open"),
18266 Err(error) => error,
18267 };
18268 assert!(
18269 error.to_string().contains("composite foreign key"),
18270 "{error}"
18271 );
18272
18273 let conn = Connection::open(&path).expect("inspect rejected database");
18274 let user_version: i64 = conn
18275 .query_row("PRAGMA user_version", [], |row| row.get(0))
18276 .expect("user version");
18277 assert_eq!(user_version, 0);
18278 let host_tables: i64 = conn
18279 .query_row(
18280 "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN ('parents', 'children')",
18281 [],
18282 |row| row.get(0),
18283 )
18284 .expect("host table count");
18285 assert_eq!(host_tables, 0);
18286 }
18287
18288 #[test]
18289 fn sqlite_session_representation_preserves_upsert_but_loses_primary_key_update_intent() {
18290 let conn = Connection::open_in_memory().expect("open");
18291 conn.execute_batch(
18292 "CREATE TABLE things (
18293 id TEXT PRIMARY KEY,
18294 body TEXT NOT NULL,
18295 _updated_at TEXT NOT NULL
18296 ) STRICT;
18297 INSERT INTO things VALUES ('old', 'base', '0000000001000-0000-writer');",
18298 )
18299 .expect("schema and seed");
18300 let tables = vec![things_table(crate::sync::session::RowIdentity::SharedKey)];
18301
18302 let mut primary_key_session = attach_session(&conn, &tables).expect("attach session");
18303 let primary_key_tx = conn.unchecked_transaction().expect("transaction");
18304 primary_key_tx
18305 .execute(
18306 "UPDATE things SET id = 'new', _updated_at = '0000000002000-0000-writer' WHERE id = 'old'",
18307 [],
18308 )
18309 .expect("update primary key");
18310 let primary_key_changes =
18311 capture_changeset(&mut primary_key_session).expect("capture primary-key update");
18312 drop(primary_key_tx);
18313 drop(primary_key_session);
18314 let primary_key_changes =
18315 crate::changeset::walk(&primary_key_changes).expect("walk primary-key update");
18316 assert_eq!(
18317 primary_key_changes
18318 .iter()
18319 .map(|change| (change.op, change.pk()))
18320 .collect::<Vec<_>>(),
18321 vec![
18322 (crate::changeset::ChangeOp::Insert, Some("new")),
18323 (crate::changeset::ChangeOp::Delete, Some("old")),
18324 ]
18325 );
18326
18327 let mut upsert_session = attach_session(&conn, &tables).expect("attach session");
18328 let upsert_tx = conn.unchecked_transaction().expect("transaction");
18329 upsert_tx
18330 .execute(
18331 "INSERT INTO things VALUES ('old', 'upserted', '0000000003000-0000-writer')
18332 ON CONFLICT(id) DO UPDATE SET
18333 body = excluded.body,
18334 _updated_at = excluded._updated_at",
18335 [],
18336 )
18337 .expect("same-id upsert");
18338 let upsert_changes = capture_changeset(&mut upsert_session).expect("capture upsert");
18339 drop(upsert_tx);
18340 let upsert_changes = crate::changeset::walk(&upsert_changes).expect("walk upsert");
18341 assert_eq!(upsert_changes.len(), 1);
18342 assert_eq!(upsert_changes[0].op, crate::changeset::ChangeOp::Update);
18343 assert_eq!(upsert_changes[0].pk(), Some("old"));
18344 }
18345
18346 #[test]
18347 fn writer_and_read_only_open_reject_existing_invalid_independent_uuid() {
18348 let writer_error = match Database::open(
18349 Path::new(":memory:"),
18350 vec![things_table(
18351 crate::sync::session::RowIdentity::IndependentUuid,
18352 )],
18353 BLOB_TOMBSTONE_GRACE,
18354 crate::blob::TransferLimits::serial(),
18355 crate::WritePolicy::MergeConcurrent,
18356 "invalid-uuid-writer".to_string(),
18357 &[Migration::sql(
18358 1,
18359 "things",
18360 "CREATE TABLE things (
18361 id TEXT PRIMARY KEY,
18362 body TEXT NOT NULL,
18363 _updated_at TEXT NOT NULL
18364 ) STRICT;
18365 INSERT INTO things VALUES ('2', 'invalid', '0000000001000-0000-seed');",
18366 )],
18367 ) {
18368 Ok(_) => panic!("writer open must reject an existing non-UUID id"),
18369 Err(error) => error.to_string(),
18370 };
18371 assert!(
18372 writer_error.contains("things") && writer_error.contains("\"2\""),
18373 "writer error identifies the table and value: {writer_error}",
18374 );
18375
18376 let dir = tempfile::tempdir().expect("temp dir");
18377 let path = dir.path().join("read-only-invalid.sqlite");
18378 let (writer, _) = Database::open(
18379 &path,
18380 vec![things_table(crate::sync::session::RowIdentity::SharedKey)],
18381 BLOB_TOMBSTONE_GRACE,
18382 crate::blob::TransferLimits::serial(),
18383 crate::WritePolicy::MergeConcurrent,
18384 "invalid-uuid-seed".to_string(),
18385 &[Migration::sql(
18386 1,
18387 "things",
18388 "CREATE TABLE things (
18389 id TEXT PRIMARY KEY,
18390 body TEXT NOT NULL,
18391 _updated_at TEXT NOT NULL
18392 ) STRICT;
18393 INSERT INTO things VALUES ('2', 'invalid', '0000000001000-0000-seed');",
18394 )],
18395 )
18396 .expect("seed database under its declared SharedKey contract");
18397 drop(writer);
18398
18399 let reader_error = match Database::open_read_only(
18400 &path,
18401 vec![things_table(
18402 crate::sync::session::RowIdentity::IndependentUuid,
18403 )],
18404 BLOB_TOMBSTONE_GRACE,
18405 crate::blob::TransferLimits::serial(),
18406 crate::WritePolicy::MergeConcurrent,
18407 "invalid-uuid-reader".to_string(),
18408 &[things_migration()],
18409 ) {
18410 Ok(_) => panic!("read-only open must reject an existing non-UUID id"),
18411 Err(error) => error.to_string(),
18412 };
18413 assert!(
18414 reader_error.contains("things") && reader_error.contains("\"2\""),
18415 "reader error identifies the table and value: {reader_error}",
18416 );
18417 }
18418
18419 #[test]
18420 fn database_open_rejects_duplicate_synced_table_declarations() {
18421 let error = match Database::open(
18422 Path::new(":memory:"),
18423 vec![
18424 things_table(crate::sync::session::RowIdentity::SharedKey),
18425 things_table(crate::sync::session::RowIdentity::IndependentUuid),
18426 ],
18427 BLOB_TOMBSTONE_GRACE,
18428 crate::blob::TransferLimits::serial(),
18429 crate::WritePolicy::MergeConcurrent,
18430 "duplicate-things".to_string(),
18431 &[things_migration()],
18432 ) {
18433 Ok(_) => panic!("one table cannot have two identity declarations"),
18434 Err(error) => error.to_string(),
18435 };
18436 assert!(
18437 error.contains("things") && error.contains("declared"),
18438 "{error}"
18439 );
18440 }
18441
18442 #[tokio::test]
18443 async fn invalid_host_identity_rolls_back_rows_and_preserves_existing_write() {
18444 let tables = vec![things_table(
18445 crate::sync::session::RowIdentity::IndependentUuid,
18446 )];
18447 let (db, _) = Database::open(
18448 Path::new(":memory:"),
18449 tables.clone(),
18450 BLOB_TOMBSTONE_GRACE,
18451 crate::blob::TransferLimits::serial(),
18452 crate::WritePolicy::MergeConcurrent,
18453 "invalid-host-identity".to_string(),
18454 &[things_migration()],
18455 )
18456 .expect("open");
18457 let existing_changeset = vec![0x45, 0x58, 0x41, 0x43, 0x54];
18458 let existing_for_insert = existing_changeset.clone();
18459 db.call(move |conn| {
18460 conn.execute(
18461 "INSERT INTO store_writes
18462 (write_id, status, affected_rows, changeset, inverse_changeset, base, blob_facts)
18463 VALUES (
18464 'existing-write', '\"pending\"', '[]', ?1, ?1,
18465 '{\"merge_concurrent\":{\"dependencies\":{}}}',
18466 '{\"blobs\":[]}'
18467 )",
18468 [existing_for_insert],
18469 )
18470 .map(|_| ())
18471 .map_err(DbError::from)
18472 })
18473 .await
18474 .expect("seed existing write records");
18475
18476 let write_id = db.new_write_id();
18477 let result = db
18478 .call(move |conn| {
18479 Database::run_internal_store_write_transaction_on(
18480 conn,
18481 &tables,
18482 crate::WritePolicy::MergeConcurrent,
18483 None,
18484 write_id,
18485 |tx| {
18486 tx.execute(
18487 "INSERT INTO things VALUES (?1, 'valid', '0000000002000-0000-writer')",
18488 ["f47ac10b-58cc-4372-a567-0e02b2c3d479"],
18489 )?;
18490 tx.execute(
18491 "INSERT INTO things VALUES ('2', 'invalid', '0000000002001-0000-writer')",
18492 [],
18493 )?;
18494 Ok::<_, DbError>(())
18495 },
18496 )
18497 })
18498 .await;
18499 let error = result.expect_err("invalid UUID must reject the host transaction");
18500 assert!(error.to_string().contains("things") && error.to_string().contains("2"));
18501
18502 db.call(move |conn| {
18503 let row_count: i64 = conn
18504 .query_row("SELECT COUNT(*) FROM things", [], |row| row.get(0))
18505 .map_err(DbError::from)?;
18506 let pending = conn
18507 .prepare("SELECT changeset FROM store_writes ORDER BY ordinal")
18508 .and_then(|mut statement| {
18509 statement
18510 .query_map([], |row| row.get::<_, Vec<u8>>(0))?
18511 .collect::<rusqlite::Result<Vec<_>>>()
18512 })
18513 .map_err(DbError::from)?;
18514 assert_eq!(row_count, 0);
18515 assert_eq!(pending, vec![existing_changeset]);
18516 Ok(())
18517 })
18518 .await
18519 .expect("inspect rollback");
18520 }
18521
18522 #[tokio::test]
18523 async fn valid_identity_changes_updates_and_upserts_succeed_but_invalid_new_uuid_rolls_back() {
18524 let tables = vec![things_table(
18525 crate::sync::session::RowIdentity::IndependentUuid,
18526 )];
18527 let (db, _) = Database::open(
18528 Path::new(":memory:"),
18529 tables.clone(),
18530 BLOB_TOMBSTONE_GRACE,
18531 crate::blob::TransferLimits::serial(),
18532 crate::WritePolicy::MergeConcurrent,
18533 "host-identity-changes".to_string(),
18534 &[things_migration()],
18535 )
18536 .expect("open");
18537 let original = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
18538 db.call(move |conn| {
18539 conn.execute(
18540 "INSERT INTO things VALUES (?1, 'base', '0000000001000-0000-writer')",
18541 [original],
18542 )
18543 .map(|_| ())
18544 .map_err(DbError::from)
18545 })
18546 .await
18547 .expect("seed row");
18548
18549 let update_tables = tables.clone();
18550 let renamed = "01890a5d-ac96-774b-bcce-b302099c3f74";
18551 let update_write_id = db.new_write_id();
18552 db.call(move |conn| {
18553 Database::run_internal_store_write_transaction_on(
18554 conn,
18555 &update_tables,
18556 crate::WritePolicy::MergeConcurrent,
18557 None,
18558 update_write_id,
18559 |tx| {
18560 tx.execute(
18561 "UPDATE things SET id = ?1, _updated_at = '0000000002000-0000-writer' WHERE id = ?2",
18562 [renamed, original],
18563 )?;
18564 Ok::<_, DbError>(())
18565 },
18566 )
18567 })
18568 .await
18569 .expect("valid primary-key change succeeds");
18570
18571 let replace_tables = tables.clone();
18572 let replaced = "8b1a9953-c461-4e20-8c66-826115d53552";
18573 let replace_write_id = db.new_write_id();
18574 db.call(move |conn| {
18575 Database::run_internal_store_write_transaction_on(
18576 conn,
18577 &replace_tables,
18578 crate::WritePolicy::MergeConcurrent,
18579 None,
18580 replace_write_id,
18581 |tx| {
18582 tx.execute("DELETE FROM things WHERE id = ?1", [renamed])?;
18583 tx.execute(
18584 "INSERT INTO things VALUES (?1, 'replaced', '0000000003000-0000-writer')",
18585 [replaced],
18586 )?;
18587 Ok::<_, DbError>(())
18588 },
18589 )
18590 })
18591 .await
18592 .expect("explicit delete and insert succeeds");
18593
18594 let ordinary_tables = tables.clone();
18595 let ordinary_write_id = db.new_write_id();
18596 db.call(move |conn| {
18597 Database::run_internal_store_write_transaction_on(
18598 conn,
18599 &ordinary_tables,
18600 crate::WritePolicy::MergeConcurrent,
18601 None,
18602 ordinary_write_id,
18603 |tx| {
18604 tx.execute(
18605 "UPDATE things SET body = 'ordinary', _updated_at = '0000000004000-0000-writer' WHERE id = ?1",
18606 [replaced],
18607 )?;
18608 tx.execute(
18609 "INSERT INTO things VALUES (?1, 'upserted', '0000000005000-0000-writer')
18610 ON CONFLICT(id) DO UPDATE SET body = excluded.body, _updated_at = excluded._updated_at",
18611 [replaced],
18612 )?;
18613 Ok::<_, DbError>(())
18614 },
18615 )
18616 })
18617 .await
18618 .expect("ordinary update and same-id upsert succeed");
18619
18620 let pending_before = db
18621 .call(|conn| {
18622 conn.prepare("SELECT changeset FROM store_writes ORDER BY ordinal")
18623 .and_then(|mut statement| {
18624 statement
18625 .query_map([], |row| row.get::<_, Vec<u8>>(0))?
18626 .collect::<rusqlite::Result<Vec<_>>>()
18627 })
18628 .map_err(DbError::from)
18629 })
18630 .await
18631 .expect("read existing write records");
18632 assert_eq!(pending_before.len(), 3);
18633
18634 let invalid_tables = tables;
18635 let invalid_write_id = db.new_write_id();
18636 let invalid = db
18637 .call(move |conn| {
18638 Database::run_internal_store_write_transaction_on(
18639 conn,
18640 &invalid_tables,
18641 crate::WritePolicy::MergeConcurrent,
18642 None,
18643 invalid_write_id,
18644 |tx| {
18645 tx.execute(
18646 "UPDATE things SET id = 'not-a-uuid', _updated_at = '0000000006000-0000-writer' WHERE id = ?1",
18647 [replaced],
18648 )?;
18649 Ok::<_, DbError>(())
18650 },
18651 )
18652 })
18653 .await;
18654 let error = invalid.expect_err("invalid new UUID rejects the primary-key change");
18655 assert!(error.to_string().contains("not-a-uuid"));
18656
18657 db.call(move |conn| {
18658 let row = conn
18659 .query_row("SELECT id, body FROM things", [], |row| {
18660 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
18661 })
18662 .map_err(DbError::from)?;
18663 let pending_after = conn
18664 .prepare("SELECT changeset FROM store_writes ORDER BY ordinal")
18665 .and_then(|mut statement| {
18666 statement
18667 .query_map([], |row| row.get::<_, Vec<u8>>(0))?
18668 .collect::<rusqlite::Result<Vec<_>>>()
18669 })
18670 .map_err(DbError::from)?;
18671 assert_eq!(row, (replaced.to_string(), "upserted".to_string()));
18672 assert_eq!(pending_after, pending_before);
18673 Ok(())
18674 })
18675 .await
18676 .expect("invalid identity change rolls back row and write records");
18677 }
18678
18679 #[tokio::test]
18687 async fn slow_db_call_does_not_block_the_executor() {
18688 use std::time::{Duration, Instant};
18689
18690 let (db, _stamper) = Database::open(
18691 Path::new(":memory:"),
18692 Vec::new(),
18693 BLOB_TOMBSTONE_GRACE,
18694 crate::blob::TransferLimits::serial(),
18695 crate::WritePolicy::MergeConcurrent,
18696 "liveness".to_string(),
18697 &[],
18698 )
18699 .expect("open database");
18700
18701 let slow_db = db.clone();
18702 let slow = tokio::spawn(async move {
18703 slow_db
18704 .call(|conn| {
18705 std::thread::sleep(Duration::from_millis(500));
18706 conn.query_row("SELECT 1", [], |r| r.get::<_, i64>(0))
18707 .map_err(DbError::from)
18708 })
18709 .await
18710 });
18711
18712 let start = Instant::now();
18713 tokio::task::yield_now().await;
18714 let stalled = start.elapsed();
18715
18716 assert!(
18717 stalled < Duration::from_millis(250),
18718 "unrelated task stalled {stalled:?} behind the slow DB call — the executor was blocked",
18719 );
18720
18721 let value = slow
18722 .await
18723 .expect("slow DB task joins")
18724 .expect("slow DB call succeeds");
18725 assert_eq!(value, 1, "the slow DB call still returns its result");
18726 }
18727
18728 #[tokio::test]
18729 async fn prepared_audience_objects_reload_the_same_verified_bytes_and_spool() {
18730 let (db, _stamper) = Database::open(
18731 Path::new(":memory:"),
18732 Vec::new(),
18733 BLOB_TOMBSTONE_GRACE,
18734 crate::blob::TransferLimits::serial(),
18735 crate::WritePolicy::MergeConcurrent,
18736 "prepared-audience-objects".to_string(),
18737 &[],
18738 )
18739 .expect("open database");
18740 let write_id = WriteId::from_generated("write-1".to_string());
18741 let stored_write_id = write_id.clone();
18742 db.call(move |conn| {
18743 let base = serde_json::to_string(&StoreWriteBase::MergeConcurrent {
18744 dependencies: BTreeMap::new(),
18745 })
18746 .expect("serialize base");
18747 conn.execute(
18748 "INSERT INTO store_writes
18749 (write_id, status, affected_rows, changeset, inverse_changeset, base, blob_facts)
18750 VALUES (?1, '\"pending\"', '[]', X'', X'', ?2, '{\"blobs\":[]}')",
18751 rusqlite::params![stored_write_id.as_str(), base],
18752 )
18753 .map(|_| ())
18754 .map_err(DbError::from)
18755 })
18756 .await
18757 .expect("seed write");
18758
18759 let binding = exact_blob_binding("photo", "0000000001000-0000-a", b"photo bytes");
18760 let second_object = ExactObjectRef::new(
18761 crate::storage::cloud::ObjectSlot::opaque(
18762 binding.blob().locator().semantic_key(),
18763 "database-test-second-physical-object".to_string(),
18764 )
18765 .expect("second blob slot"),
18766 binding.blob().object().stored_size(),
18767 binding.blob().object().stored_hash(),
18768 );
18769 let second_blob = StoredBlobRef::new(binding.blob().locator().clone(), second_object)
18770 .expect("second exact object for the same locator");
18771 let second_binding = RowBlobLocatorBinding::new(
18772 "photos",
18773 "photo-copy",
18774 "0000000001000-0000-a",
18775 "id",
18776 second_blob.clone(),
18777 )
18778 .expect("second row binding");
18779 let package = AudiencePackage::store(
18780 ObjectHash::digest(b"root"),
18781 test_candidate_family(),
18782 write_id.clone(),
18783 test_commit_coord(),
18784 1,
18785 b"changeset".to_vec(),
18786 vec![binding.clone(), second_binding],
18787 )
18788 .expect("build package");
18789 let semantic = package.to_bytes();
18790 let stored_package = b"stored package representation".to_vec();
18791 let StoreCommitCoord::MergeConcurrent { stream_id, .. } = test_commit_coord() else {
18792 unreachable!("database test commit is MergeConcurrent")
18793 };
18794 let package_slot = crate::storage::cloud::ObjectSlot::logical(format!(
18795 "{}.pkg",
18796 crate::sync::store_commit::package_semantic_prefix(
18797 test_candidate_family(),
18798 &stream_id.to_string(),
18799 1,
18800 ObjectHash::digest(&semantic),
18801 )
18802 ))
18803 .expect("package slot");
18804 let package_object = ExactObjectRef::new(
18805 package_slot,
18806 stored_package.len() as u64,
18807 ObjectHash::digest(&stored_package),
18808 );
18809 let owner_commit_hash = ObjectHash::digest(b"owner commit semantic bytes");
18810 let owner_object = ExactObjectRef::new(
18811 crate::storage::cloud::ObjectSlot::logical(format!(
18812 "{}.json",
18813 commit_semantic_prefix(
18814 test_candidate_family(),
18815 &stream_id.to_string(),
18816 1,
18817 owner_commit_hash,
18818 )
18819 ))
18820 .expect("owner commit slot"),
18821 1,
18822 ObjectHash::digest(b"owner commit"),
18823 );
18824 let owner = StoreBatchCommitRef {
18825 coord: test_commit_coord(),
18826 commit_hash: owner_commit_hash,
18827 object: owner_object,
18828 };
18829 let package_remote = crate::sync::remote_object::RemoteObjectRecord::CandidateExclusive(
18830 crate::sync::remote_object::CandidateObjectRecord {
18831 identity: crate::sync::remote_object::CandidateExclusiveTarget {
18832 family: package.candidate_family(),
18833 domain:
18834 crate::sync::remote_object::CandidateExclusiveObjectDomain::StorePackage,
18835 semantic_hash: ObjectHash::digest(&semantic),
18836 object: package_object.clone(),
18837 },
18838 bytes: crate::sync::remote_object::RemoteObjectBytes::inline(
18839 semantic.clone(),
18840 stored_package.clone(),
18841 package_object.clone(),
18842 )
18843 .expect("package remote bytes"),
18844 state: crate::sync::remote_object::CandidateObjectState::Prepared {
18845 ownership: crate::sync::remote_object::PendingCandidateOwnership {
18846 pending: std::collections::BTreeSet::from([owner.clone()]),
18847 nonactivated: Vec::new(),
18848 },
18849 },
18850 },
18851 );
18852 let package_remote_id = package_remote.object_id();
18853 let prepared_package = PreparedAudiencePackage::new(
18854 package_remote_id,
18855 semantic,
18856 stored_package.clone(),
18857 package_object.clone(),
18858 )
18859 .expect("prepare package");
18860
18861 let directory = tempfile::tempdir().expect("temp dir");
18862 let spool = directory.path().join("blob.spool");
18863 crate::local_blob::write_atomic_durable(&spool, b"stored representation")
18864 .await
18865 .expect("write spool");
18866 let blob_remote = crate::sync::remote_object::RemoteObjectRecord::SharedLiveSet(
18867 crate::sync::remote_object::SharedObjectRecord {
18868 identity: crate::sync::remote_object::SharedLiveSetObjectRef {
18869 domain: crate::sync::remote_object::SharedLiveSetObjectDomain::StoredBlob,
18870 semantic_hash: ObjectHash::digest(&binding.blob().locator().to_bytes()),
18871 object: binding.blob().object().clone(),
18872 },
18873 bytes: crate::sync::remote_object::RemoteObjectBytes::blob(
18874 binding.blob().locator().to_bytes(),
18875 binding.blob().object().clone(),
18876 )
18877 .expect("blob remote bytes"),
18878 state: crate::sync::remote_object::OwnedObjectState::Prepared {
18879 ownership: crate::sync::remote_object::PendingCandidateOwnership {
18880 pending: std::collections::BTreeSet::from([owner.clone()]),
18881 nonactivated: Vec::new(),
18882 },
18883 },
18884 },
18885 );
18886 let blob_remote_id = blob_remote.object_id();
18887 let prepared_blob = PreparedAudienceBlob {
18888 remote_object_id: blob_remote_id,
18889 audience: RemoteAudience::Store,
18890 blob: binding.blob().clone(),
18891 spool_path: Some(spool.clone()),
18892 };
18893 let second_blob_remote = crate::sync::remote_object::RemoteObjectRecord::SharedLiveSet(
18894 crate::sync::remote_object::SharedObjectRecord {
18895 identity: crate::sync::remote_object::SharedLiveSetObjectRef {
18896 domain: crate::sync::remote_object::SharedLiveSetObjectDomain::StoredBlob,
18897 semantic_hash: ObjectHash::digest(&second_blob.locator().to_bytes()),
18898 object: second_blob.object().clone(),
18899 },
18900 bytes: crate::sync::remote_object::RemoteObjectBytes::blob(
18901 second_blob.locator().to_bytes(),
18902 second_blob.object().clone(),
18903 )
18904 .expect("second blob remote bytes"),
18905 state: crate::sync::remote_object::OwnedObjectState::Prepared {
18906 ownership: crate::sync::remote_object::PendingCandidateOwnership {
18907 pending: std::collections::BTreeSet::from([owner]),
18908 nonactivated: Vec::new(),
18909 },
18910 },
18911 },
18912 );
18913 let second_blob_remote_id = second_blob_remote.object_id();
18914 let second_prepared_blob = PreparedAudienceBlob {
18915 remote_object_id: second_blob_remote_id,
18916 audience: RemoteAudience::Store,
18917 blob: second_blob,
18918 spool_path: Some(spool.clone()),
18919 };
18920 let persisted_write_id = write_id.clone();
18921 let package_state = serde_json::to_string(&package_remote).expect("package remote state");
18922 let blob_state = serde_json::to_string(&blob_remote).expect("blob remote state");
18923 let second_blob_state =
18924 serde_json::to_string(&second_blob_remote).expect("second blob remote state");
18925 db.call(move |conn| {
18926 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
18927 tx.execute(
18928 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
18929 rusqlite::params![package_remote_id.to_string(), package_state],
18930 )
18931 .map_err(DbError::from)?;
18932 tx.execute(
18933 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
18934 rusqlite::params![blob_remote_id.to_string(), blob_state],
18935 )
18936 .map_err(DbError::from)?;
18937 tx.execute(
18938 "INSERT INTO remote_objects (object_id, state) VALUES (?1, ?2)",
18939 rusqlite::params![second_blob_remote_id.to_string(), second_blob_state],
18940 )
18941 .map_err(DbError::from)?;
18942 Database::persist_prepared_audience_objects_on(
18943 &tx,
18944 &persisted_write_id,
18945 &[prepared_package],
18946 &[prepared_blob, second_prepared_blob],
18947 )?;
18948 tx.commit().map_err(DbError::from)
18949 })
18950 .await
18951 .expect("persist prepared objects");
18952
18953 let reloaded = db
18954 .prepared_audience_objects(&write_id)
18955 .await
18956 .expect("reload prepared objects");
18957 assert_eq!(reloaded.packages.len(), 1);
18958 assert_eq!(reloaded.packages[0].package(), &package);
18959 assert_eq!(reloaded.packages[0].remote_object_id(), package_remote_id);
18960 assert_eq!(reloaded.blobs.len(), 2);
18961 let reloaded_ids = reloaded
18962 .blobs
18963 .iter()
18964 .map(PreparedAudienceBlob::remote_object_id)
18965 .collect::<std::collections::BTreeSet<_>>();
18966 assert_eq!(
18967 reloaded_ids,
18968 std::collections::BTreeSet::from([blob_remote_id, second_blob_remote_id])
18969 );
18970 assert!(reloaded
18971 .blobs
18972 .iter()
18973 .all(|blob| blob.spool_path() == Some(spool.as_path())));
18974 assert_eq!(
18975 reloaded
18976 .blobs
18977 .iter()
18978 .map(|blob| blob.blob().locator().locator_hash())
18979 .collect::<std::collections::BTreeSet<_>>()
18980 .len(),
18981 1,
18982 "same-locator exact objects survive persistence independently",
18983 );
18984 }
18985
18986 #[tokio::test]
18992 async fn dropping_last_handle_in_async_context_does_not_stall_but_job_still_lands() {
18993 use std::time::{Duration, Instant};
18994
18995 let dir = tempfile::tempdir().expect("temp dir");
18996 let db_path = dir.path().join("db.sqlite");
18997 let marker = dir.path().join("marker");
18998
18999 let (db, _stamper) = Database::open(
19000 &db_path,
19001 Vec::new(),
19002 BLOB_TOMBSTONE_GRACE,
19003 crate::blob::TransferLimits::serial(),
19004 crate::WritePolicy::MergeConcurrent,
19005 "drop-async".to_string(),
19006 &[],
19007 )
19008 .expect("open");
19009
19010 let job_db = db.clone();
19015 let job_marker = marker.clone();
19016 let task = tokio::spawn(async move {
19017 let _ = job_db
19018 .call(move |_conn| {
19019 std::thread::sleep(Duration::from_millis(300));
19020 std::fs::write(&job_marker, b"landed")
19021 .map_err(|e| DbError::Message(e.to_string()))
19022 })
19023 .await;
19024 });
19025 tokio::task::yield_now().await;
19026 task.abort();
19027 let _ = task.await;
19028
19029 let drop_start = Instant::now();
19032 drop(db);
19033 let drop_elapsed = drop_start.elapsed();
19034 assert!(
19035 drop_elapsed < Duration::from_millis(200),
19036 "dropping the last handle stalled {drop_elapsed:?} — it joined the connection thread \
19037 instead of detaching",
19038 );
19039
19040 let deadline = Instant::now() + Duration::from_secs(5);
19042 while !marker.exists() {
19043 assert!(
19044 Instant::now() < deadline,
19045 "the dispatched job's effect never landed after the last handle dropped",
19046 );
19047 tokio::time::sleep(Duration::from_millis(10)).await;
19048 }
19049 }
19050
19051 #[tokio::test]
19052 async fn database_open_rejects_empty_device_id() {
19053 let result = Database::open(
19054 Path::new(":memory:"),
19055 Vec::new(),
19056 BLOB_TOMBSTONE_GRACE,
19057 crate::blob::TransferLimits::serial(),
19058 crate::WritePolicy::MergeConcurrent,
19059 String::new(),
19060 &[],
19061 );
19062 let error = match result {
19063 Ok(_) => panic!("empty device_id must be rejected"),
19064 Err(error) => error.to_string(),
19065 };
19066
19067 assert!(
19068 error.contains("device_id") && error.contains("empty"),
19069 "error names the empty device id: {error}",
19070 );
19071 }
19072
19073 #[test]
19074 fn host_sql_authorizer_is_removed_after_success_error_and_panic() {
19075 let conn = Connection::open_in_memory().expect("open");
19076 conn.execute_batch(
19077 "ATTACH ':memory:' AS coven_gate_empty; \
19078 CREATE TABLE coven_gate_empty.baseline (id TEXT PRIMARY KEY) STRICT; \
19079 INSERT INTO coven_gate_empty.baseline VALUES ('guarded');",
19080 )
19081 .expect("attach guarded schema");
19082 let assert_guard_removed = || {
19083 let id: String = conn
19084 .query_row("SELECT id FROM coven_gate_empty.baseline", [], |row| {
19085 row.get(0)
19086 })
19087 .expect("internal SQL can address the baseline after host SQL");
19088 assert_eq!(id, "guarded");
19089 };
19090
19091 Database::run_host_sql_on(&conn, || Ok::<_, DbError>(())).expect("successful host SQL");
19092 assert_guard_removed();
19093
19094 let error =
19095 Database::run_host_sql_on(&conn, || Err::<(), _>(DbError::Message("host".into())));
19096 assert!(error.is_err());
19097 assert_guard_removed();
19098
19099 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
19100 Database::run_host_sql_on(&conn, || -> Result<(), DbError> { panic!("host panic") })
19101 .expect("panicking host SQL closure never returns");
19102 }));
19103 assert!(panic.is_err());
19104 assert_guard_removed();
19105 }
19106
19107 #[tokio::test]
19108 async fn database_open_rejects_host_declared_reserved_tables() {
19109 for table_name in ["cloud_outbox", "protocol_state"] {
19110 let result = Database::open(
19111 Path::new(":memory:"),
19112 vec![SyncedTable::new(
19113 table_name,
19114 crate::sync::session::RowIdentity::SharedKey,
19115 )],
19116 BLOB_TOMBSTONE_GRACE,
19117 crate::blob::TransferLimits::serial(),
19118 crate::WritePolicy::MergeConcurrent,
19119 format!("reserved-{table_name}"),
19120 &[notes_migration()],
19121 );
19122 let error = match result {
19123 Ok(_) => panic!("reserved table {table_name} must be rejected"),
19124 Err(error) => error.to_string(),
19125 };
19126
19127 assert!(
19128 error.contains(table_name),
19129 "error names reserved table {table_name}: {error}",
19130 );
19131 }
19132 }
19133
19134 #[tokio::test]
19135 async fn database_open_rejects_empty_synced_table_name() {
19136 let result = Database::open(
19137 Path::new(":memory:"),
19138 vec![SyncedTable::new(
19139 "",
19140 crate::sync::session::RowIdentity::SharedKey,
19141 )],
19142 BLOB_TOMBSTONE_GRACE,
19143 crate::blob::TransferLimits::serial(),
19144 crate::WritePolicy::MergeConcurrent,
19145 "empty-synced-table".to_string(),
19146 &[notes_migration()],
19147 );
19148 let error = match result {
19149 Ok(_) => panic!("empty synced table name must be rejected"),
19150 Err(error) => error.to_string(),
19151 };
19152
19153 assert!(
19154 error.contains("empty"),
19155 "error names empty synced table: {error}",
19156 );
19157 }
19158
19159 #[tokio::test]
19160 async fn database_open_accepts_normal_host_synced_table() {
19161 Database::open(
19162 Path::new(":memory:"),
19163 vec![SyncedTable::new(
19164 "notes",
19165 crate::sync::session::RowIdentity::SharedKey,
19166 )],
19167 BLOB_TOMBSTONE_GRACE,
19168 crate::blob::TransferLimits::serial(),
19169 crate::WritePolicy::MergeConcurrent,
19170 "normal-synced-table".to_string(),
19171 &[notes_migration()],
19172 )
19173 .expect("normal host table opens");
19174 }
19175
19176 fn open_contract_error(
19180 migration_sql: &'static str,
19181 tables: Vec<SyncedTable>,
19182 device_id: &str,
19183 ) -> String {
19184 let result = Database::open(
19185 Path::new(":memory:"),
19186 tables,
19187 BLOB_TOMBSTONE_GRACE,
19188 crate::blob::TransferLimits::serial(),
19189 crate::WritePolicy::MergeConcurrent,
19190 device_id.to_string(),
19191 &[Migration::sql(1, "contract", migration_sql)],
19192 );
19193 match result {
19194 Ok(_) => panic!("open must reject the synced-table contract violation"),
19195 Err(error) => error.to_string(),
19196 }
19197 }
19198
19199 #[tokio::test]
19200 async fn database_open_rejects_integer_primary_key() {
19201 let error = open_contract_error(
19202 "CREATE TABLE things (id INTEGER PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19203 vec![SyncedTable::new(
19204 "things",
19205 crate::sync::session::RowIdentity::SharedKey,
19206 )],
19207 "integer-pk",
19208 );
19209 assert!(
19210 error.contains("things") && error.contains("TEXT"),
19211 "error names the table and the TEXT requirement: {error}",
19212 );
19213 }
19214
19215 #[tokio::test]
19216 async fn database_open_rejects_primary_key_not_at_column_zero() {
19217 let error = open_contract_error(
19218 "CREATE TABLE things (body TEXT NOT NULL, id TEXT PRIMARY KEY, \
19219 _updated_at TEXT NOT NULL) STRICT;",
19220 vec![SyncedTable::new(
19221 "things",
19222 crate::sync::session::RowIdentity::SharedKey,
19223 )],
19224 "pk-not-first",
19225 );
19226 assert!(
19227 error.contains("things") && error.contains("column 0"),
19228 "error names the table and the column-0 requirement: {error}",
19229 );
19230 }
19231
19232 #[tokio::test]
19233 async fn database_open_rejects_primary_key_named_other_than_id() {
19234 let error = open_contract_error(
19235 "CREATE TABLE things (thing_id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19236 vec![SyncedTable::new(
19237 "things",
19238 crate::sync::session::RowIdentity::SharedKey,
19239 )],
19240 "pk-misnamed",
19241 );
19242 assert!(
19243 error.contains("things") && error.contains("`id`"),
19244 "error names the table and the `id` requirement: {error}",
19245 );
19246 }
19247
19248 #[tokio::test]
19249 async fn database_open_rejects_composite_primary_key() {
19250 let error = open_contract_error(
19251 "CREATE TABLE things (id TEXT NOT NULL, part TEXT NOT NULL, \
19252 _updated_at TEXT NOT NULL, PRIMARY KEY (id, part)) STRICT;",
19253 vec![SyncedTable::new(
19254 "things",
19255 crate::sync::session::RowIdentity::SharedKey,
19256 )],
19257 "composite-pk",
19258 );
19259 assert!(
19260 error.contains("things") && error.contains("composite"),
19261 "error names the table and the single-primary-key requirement: {error}",
19262 );
19263 }
19264
19265 #[tokio::test]
19266 async fn database_open_rejects_nullable_updated_at() {
19267 let error = open_contract_error(
19268 "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT) STRICT;",
19269 vec![SyncedTable::new(
19270 "things",
19271 crate::sync::session::RowIdentity::SharedKey,
19272 )],
19273 "nullable-updated-at",
19274 );
19275 assert!(
19276 error.contains("things") && error.contains("_updated_at"),
19277 "error names the table and the `_updated_at` requirement: {error}",
19278 );
19279 }
19280
19281 #[tokio::test]
19282 async fn database_open_rejects_non_strict_synced_table() {
19283 let error = open_contract_error(
19284 "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL);",
19285 vec![SyncedTable::new(
19286 "things",
19287 crate::sync::session::RowIdentity::SharedKey,
19288 )],
19289 "non-strict",
19290 );
19291 assert!(
19292 error.contains("things") && error.contains("STRICT"),
19293 "error names the table and the STRICT requirement: {error}",
19294 );
19295 }
19296
19297 #[tokio::test]
19298 async fn database_open_rejects_declared_table_no_migration_creates() {
19299 let error = open_contract_error(
19300 "CREATE TABLE other (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19301 vec![SyncedTable::new(
19302 "things",
19303 crate::sync::session::RowIdentity::SharedKey,
19304 )],
19305 "declared-never-created",
19306 );
19307 assert!(
19308 error.contains("things") && error.contains("no migration creates it"),
19309 "error says the declared table was never created: {error}",
19310 );
19311 }
19312
19313 #[tokio::test]
19314 async fn database_open_rejects_synced_table_spelling_that_differs_from_live_schema() {
19315 let error = open_contract_error(
19316 "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19317 vec![SyncedTable::new(
19318 "Things",
19319 crate::sync::session::RowIdentity::SharedKey,
19320 )],
19321 "case-variant-table-name",
19322 );
19323 assert!(
19324 error.contains("Things") && error.contains("things") && error.contains("exact"),
19325 "error names both spellings and requires the live spelling: {error}",
19326 );
19327 }
19328
19329 #[tokio::test]
19330 async fn database_open_rejects_case_variant_duplicate_synced_tables() {
19331 let error = open_contract_error(
19332 "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19333 vec![
19334 SyncedTable::new("things", crate::sync::session::RowIdentity::SharedKey),
19335 SyncedTable::new("THINGS", crate::sync::session::RowIdentity::IndependentUuid),
19336 ],
19337 "case-variant-duplicate-table",
19338 );
19339 assert!(
19340 error.contains("things")
19341 && error.contains("THINGS")
19342 && error.contains("more than once"),
19343 "error names both duplicate declarations: {error}",
19344 );
19345 }
19346
19347 #[tokio::test]
19348 async fn database_open_accepts_strict_synced_table() {
19349 Database::open(
19350 Path::new(":memory:"),
19351 vec![SyncedTable::new(
19352 "things",
19353 crate::sync::session::RowIdentity::SharedKey,
19354 )],
19355 BLOB_TOMBSTONE_GRACE,
19356 crate::blob::TransferLimits::serial(),
19357 crate::WritePolicy::MergeConcurrent,
19358 "strict-synced-table".to_string(),
19359 &[Migration::sql(
19360 1,
19361 "contract",
19362 "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT;",
19363 )],
19364 )
19365 .expect("a STRICT synced table satisfying the rest of the contract opens");
19366 }
19367
19368 #[tokio::test]
19369 async fn database_open_ignores_undeclared_non_strict_local_table() {
19370 Database::open(
19374 Path::new(":memory:"),
19375 vec![SyncedTable::new(
19376 "things",
19377 crate::sync::session::RowIdentity::SharedKey,
19378 )],
19379 BLOB_TOMBSTONE_GRACE,
19380 crate::blob::TransferLimits::serial(),
19381 crate::WritePolicy::MergeConcurrent,
19382 "undeclared-local-table".to_string(),
19383 &[Migration::sql(
19384 1,
19385 "contract",
19386 "CREATE TABLE things (id TEXT PRIMARY KEY, _updated_at TEXT NOT NULL) STRICT; \
19387 CREATE TABLE scratch (id INTEGER PRIMARY KEY, note TEXT);",
19388 )],
19389 )
19390 .expect("an undeclared non-strict local table is not coven's business");
19391 }
19392
19393 #[tokio::test]
19394 async fn database_open_rejects_duplicate_blob_namespace() {
19395 let blob = |namespace| {
19396 crate::sync::session::BlobDecl::new(
19397 namespace,
19398 crate::blob::Provenance::HostProvided,
19399 crate::blob::CacheFill::CacheLazy,
19400 )
19401 };
19402 let error = open_contract_error(
19403 "CREATE TABLE covers (id TEXT PRIMARY KEY, size INTEGER NOT NULL, \
19404 hash TEXT, _updated_at TEXT NOT NULL) STRICT;\
19405 CREATE TABLE thumbs (id TEXT PRIMARY KEY, size INTEGER NOT NULL, \
19406 hash TEXT, _updated_at TEXT NOT NULL) STRICT;",
19407 vec![
19408 SyncedTable::new("covers", crate::sync::session::RowIdentity::SharedKey)
19409 .carries_blob(blob("images")),
19410 SyncedTable::new("thumbs", crate::sync::session::RowIdentity::SharedKey)
19411 .carries_blob(blob("images")),
19412 ],
19413 "dup-namespace",
19414 );
19415 assert!(
19416 error.contains("covers") && error.contains("thumbs") && error.contains("images"),
19417 "error names both tables and the shared blob namespace: {error}",
19418 );
19419 }
19420
19421 #[tokio::test]
19422 async fn fresh_open_requires_each_make_remote_intent_to_name_retain_pinned() {
19423 let (db, _stamper) = Database::open(
19424 Path::new(":memory:"),
19425 Vec::new(),
19426 BLOB_TOMBSTONE_GRACE,
19427 crate::blob::TransferLimits::serial(),
19428 crate::WritePolicy::MergeConcurrent,
19429 "test-device".to_string(),
19430 &[],
19431 )
19432 .expect("open database");
19433
19434 let column = db
19435 .call(|conn| {
19436 let mut stmt = conn
19437 .prepare("PRAGMA table_info(blob_make_remote_intents)")
19438 .map_err(DbError::from)?;
19439 let rows = stmt
19440 .query_map([], |row| {
19441 Ok((
19442 row.get::<_, String>(1)?,
19443 row.get::<_, i64>(3)?,
19444 row.get::<_, Option<String>>(4)?,
19445 ))
19446 })
19447 .map_err(DbError::from)?;
19448 for row in rows {
19449 let (name, notnull, default_value) = row.map_err(DbError::from)?;
19450 if name == "retain_pinned" {
19451 return Ok(Some((notnull, default_value)));
19452 }
19453 }
19454 Ok(None)
19455 })
19456 .await
19457 .expect("read make_remote intent schema")
19458 .expect("retain_pinned column exists");
19459
19460 assert_eq!(column.0, 1, "retain_pinned must be NOT NULL");
19461 assert_eq!(
19462 column.1, None,
19463 "retain_pinned must be supplied by every make_remote intent",
19464 );
19465 }
19466
19467 #[tokio::test]
19468 async fn serial_pending_branch_survives_reopen_with_exact_base_and_inverses() {
19469 let temp = tempfile::tempdir().expect("temporary Store");
19470 let path = temp.path().join("serial.db");
19471 let tables = vec![SyncedTable::new(
19472 "notes",
19473 crate::sync::session::RowIdentity::SharedKey,
19474 )];
19475 let migrations = vec![notes_migration()];
19476 let (db, _) = Database::open(
19477 &path,
19478 tables.clone(),
19479 BLOB_TOMBSTONE_GRACE,
19480 crate::blob::TransferLimits::serial(),
19481 crate::WritePolicy::Serial,
19482 "serial-device".to_string(),
19483 &migrations,
19484 )
19485 .expect("open serial Store");
19486 for (write_id, sql) in [
19487 (
19488 "serial-write-1",
19489 "INSERT INTO notes VALUES ('n1', 'first', '0000000001000-0000-serial')",
19490 ),
19491 (
19492 "serial-write-2",
19493 "UPDATE notes SET body = 'second', _updated_at = '0000000002000-0000-serial' WHERE id = 'n1'",
19494 ),
19495 ] {
19496 let tables = tables.clone();
19497 let write_id = WriteId::from_generated(write_id.to_string());
19498 db.call(move |conn| {
19499 Database::run_internal_store_write_transaction_on(
19500 conn,
19501 &tables,
19502 crate::WritePolicy::Serial,
19503 None,
19504 write_id,
19505 |tx| tx.execute_batch(sql).map_err(DbError::from),
19506 )
19507 })
19508 .await
19509 .expect("commit provisional serial write");
19510 }
19511 drop(db);
19512
19513 let (reopened, _) = Database::open(
19514 &path,
19515 tables,
19516 BLOB_TOMBSTONE_GRACE,
19517 crate::blob::TransferLimits::serial(),
19518 crate::WritePolicy::Serial,
19519 "serial-device".to_string(),
19520 &migrations,
19521 )
19522 .expect("reopen serial Store");
19523 let rows = reopened
19524 .call(|conn| {
19525 let mut statement = conn
19526 .prepare(
19527 "SELECT write_id, inverse_changeset, base
19528 FROM store_writes ORDER BY ordinal",
19529 )
19530 .map_err(DbError::from)?;
19531 let rows = statement
19532 .query_map([], |row| {
19533 Ok((
19534 row.get::<_, String>(0)?,
19535 row.get::<_, Vec<u8>>(1)?,
19536 row.get::<_, String>(2)?,
19537 ))
19538 })
19539 .map_err(DbError::from)?
19540 .collect::<Result<Vec<_>, _>>()
19541 .map_err(DbError::from)?;
19542 Ok(rows)
19543 })
19544 .await
19545 .expect("read reopened branch");
19546 assert_eq!(rows.len(), 2);
19547 for (write_id, inverse, base) in rows {
19548 assert!(!inverse.is_empty(), "{write_id} retains its inverse");
19549 let base: StoreWriteBase = serde_json::from_str(&base).expect("serial base");
19550 assert_eq!(
19551 base,
19552 StoreWriteBase::Serial {
19553 branch_id: PendingBranchId::from_first_write(WriteId::from_generated(
19554 "serial-write-1".to_string(),
19555 )),
19556 base: None,
19557 }
19558 );
19559 }
19560 }
19561
19562 const RESTART_CIRCLE_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaa";
19563
19564 fn restart_circle_coord(policy: WritePolicy) -> String {
19565 match policy {
19566 WritePolicy::MergeConcurrent => serde_json::json!({
19567 "merge_concurrent": {
19568 "device_id": "restart-control-device",
19569 "stream_id": "55".repeat(32),
19570 "author_pubkey": "restart-owner",
19571 "author_owner_grant": "11".repeat(32),
19572 "seq": 1,
19573 "control_hash": "22".repeat(32)
19574 }
19575 }),
19576 WritePolicy::Serial => serde_json::json!({
19577 "serial": {
19578 "author_pubkey": "restart-owner",
19579 "generation": 1,
19580 "control_hash": "33".repeat(32)
19581 }
19582 }),
19583 }
19584 .to_string()
19585 }
19586
19587 async fn capture_scoped_write_then_reopen(
19588 policy: WritePolicy,
19589 name: &str,
19590 ) -> (
19591 tempfile::TempDir,
19592 Database,
19593 Vec<(String, Option<String>, Vec<u8>)>,
19594 ) {
19595 let temp = tempfile::tempdir().expect("temporary scoped Store");
19596 let path = temp.path().join(format!("{name}.db"));
19597 let tables =
19598 vec![
19599 SyncedTable::new("accounts", crate::sync::session::RowIdentity::SharedKey)
19600 .scoped_by("audience"),
19601 ];
19602 let migrations = vec![Migration::sql(
19603 1,
19604 "accounts",
19605 "CREATE TABLE accounts (
19606 id TEXT PRIMARY KEY,
19607 audience TEXT,
19608 _updated_at TEXT NOT NULL
19609 ) STRICT;",
19610 )];
19611 let (db, _) = Database::open(
19612 &path,
19613 tables.clone(),
19614 BLOB_TOMBSTONE_GRACE,
19615 crate::blob::TransferLimits::serial(),
19616 policy,
19617 format!("{name}-device"),
19618 &migrations,
19619 )
19620 .expect("open scoped Store");
19621 crate::sync::test_helpers::TestStore::create(
19622 &db,
19623 name,
19624 crate::keys::UserKeypair::generate(),
19625 )
19626 .await
19627 .expect("install exact scoped Store authority");
19628 let control = restart_circle_coord(policy);
19629 db.call(move |conn| {
19630 conn.execute(
19631 "INSERT INTO circle_control_activations
19632 (circle_id, control_coord, stream_id, seq, commit_hash, control_bytes)
19633 VALUES (?1, ?2, 'restart-control-device', 1, ?3, X'01')",
19634 (RESTART_CIRCLE_ID, &control, "44".repeat(32)),
19635 )
19636 .map_err(DbError::from)?;
19637 conn.execute(
19638 "INSERT INTO circle_access_cache
19639 (circle_id, control_coord, owner_pubkey, disposition, access_bytes)
19640 VALUES (?1, ?2, 'restart-owner', 'active', X'02')",
19641 (RESTART_CIRCLE_ID, &control),
19642 )
19643 .map(|_| ())
19644 .map_err(DbError::from)
19645 })
19646 .await
19647 .expect("seed active Circle authority");
19648
19649 let gates = db.gates();
19650 let blob_decls = db.blob_decls();
19651 let write_id = db.new_write_id();
19652 let routing =
19653 (policy == WritePolicy::MergeConcurrent).then(|| EncryptionService::from_key([7; 32]));
19654 let capture_tables = tables.clone();
19655 db.call(move |conn| {
19656 Database::run_store_write_transaction_on(
19657 conn,
19658 &capture_tables,
19659 &gates,
19660 &blob_decls,
19661 policy,
19662 routing.as_ref(),
19663 write_id,
19664 |tx| {
19665 tx.execute(
19666 "INSERT INTO accounts (id, audience, _updated_at)
19667 VALUES ('store-account', NULL, '0000000001000-0000-restart')",
19668 [],
19669 )?;
19670 tx.execute(
19671 "INSERT INTO accounts (id, audience, _updated_at)
19672 VALUES ('circle-account', ?1, '0000000001001-0000-restart')",
19673 [RESTART_CIRCLE_ID],
19674 )?;
19675 Ok::<_, DbError>(())
19676 },
19677 )
19678 })
19679 .await
19680 .expect("capture Store and Circle partitions");
19681 let expected = db
19682 .call(|conn| {
19683 conn.prepare(
19684 "SELECT audience, control_coord, changeset
19685 FROM store_write_partitions
19686 ORDER BY CASE audience WHEN 'store' THEN 0 ELSE 1 END,
19687 audience, control_coord",
19688 )
19689 .and_then(|mut statement| {
19690 statement
19691 .query_map([], |row| {
19692 Ok((
19693 row.get::<_, String>(0)?,
19694 row.get::<_, Option<String>>(1)?,
19695 row.get::<_, Vec<u8>>(2)?,
19696 ))
19697 })?
19698 .collect::<rusqlite::Result<Vec<_>>>()
19699 })
19700 .map_err(DbError::from)
19701 })
19702 .await
19703 .expect("read exact persisted audience partitions");
19704 assert_eq!(expected.len(), 2);
19705 drop(db);
19706
19707 let (reopened, _) = Database::open(
19708 &path,
19709 tables,
19710 BLOB_TOMBSTONE_GRACE,
19711 crate::blob::TransferLimits::serial(),
19712 policy,
19713 format!("{name}-device"),
19714 &migrations,
19715 )
19716 .expect("reopen scoped Store");
19717 (temp, reopened, expected)
19718 }
19719
19720 fn assert_prepared_partitions(
19721 actual: &PreparedStoreWritePartitions,
19722 expected: &[(String, Option<String>, Vec<u8>)],
19723 ) {
19724 let actual = actual
19725 .iter()
19726 .map(|partition| {
19727 let audience = match partition.audience {
19728 crate::sync::circle::Audience::Store => "store".to_string(),
19729 crate::sync::circle::Audience::Circle(circle) => circle.to_string(),
19730 crate::sync::circle::Audience::Local => {
19731 panic!("Local audience entered Store preparation")
19732 }
19733 };
19734 (
19735 audience,
19736 partition.control.as_ref().map(|control| {
19737 let parsed = serde_json::from_str(control.stored_json())
19738 .expect("parse stored control");
19739 assert_eq!(control.coordinate(), &parsed);
19740 control.stored_json().to_string()
19741 }),
19742 partition.changeset.clone(),
19743 )
19744 })
19745 .collect::<Vec<_>>();
19746 assert_eq!(actual, expected);
19747 }
19748
19749 #[tokio::test]
19750 async fn merge_preparation_reloads_exact_scoped_partitions_after_restart() {
19751 let (_temp, reopened, expected) =
19752 capture_scoped_write_then_reopen(WritePolicy::MergeConcurrent, "merge-restart").await;
19753 let prepared = reopened
19754 .prepare_store_write()
19755 .await
19756 .expect("prepare restarted Merge write")
19757 .expect("pending Merge write");
19758
19759 assert_prepared_partitions(&prepared.partitions, &expected);
19760 }
19761
19762 #[tokio::test]
19763 async fn serial_preparation_reloads_exact_scoped_partitions_after_restart() {
19764 let (_temp, reopened, expected) =
19765 capture_scoped_write_then_reopen(WritePolicy::Serial, "serial-restart").await;
19766 let branch = reopened
19767 .reserve_serial_store_branch()
19768 .await
19769 .expect("reserve restarted Serial branch")
19770 .expect("pending Serial branch");
19771 assert_eq!(branch.writes.len(), 1);
19772
19773 assert_prepared_partitions(&branch.writes[0].partitions, &expected);
19774 }
19775}