1use std::cell::RefCell;
2use std::collections::VecDeque;
3use std::fs::{self, File, OpenOptions};
4use std::path::{Path, PathBuf};
5use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6use std::sync::{Arc, Mutex};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use chio_core::StoreMutationFence;
10use chio_kernel::budget_store::{
11 BudgetEventAuthority, BudgetGuaranteeLevel, RevocationCommitMetadata,
12};
13use chio_kernel::{BudgetStoreError, RevocationStoreError};
14use rusqlite::{params, Connection, OptionalExtension, Transaction, TransactionBehavior};
15
16use crate::budget_store::BUDGET_STORE_SUPPORTED_SCHEMA_VERSION;
17use crate::revocation_store::{
18 initialize_revocation_schema, verify_admission_authority_invariants,
19 REVOCATION_STORE_SUPPORTED_SCHEMA_VERSION,
20};
21use crate::{SqliteBudgetStore, SqliteRevocationStore};
22
23mod global_commit_chain;
24mod lease_history;
25mod path_identity;
26mod rollback_anchor;
27
28use global_commit_chain::{
29 append_global_commit, initialize_global_commit_schema, reset_derived_budget_ack_cache,
30 seed_global_baseline, verify_global_commit_schema, verify_pristine_authority_tables,
31};
32use lease_history::{initialize_serving_lease_schema, verify_serving_lease_history};
33use rollback_anchor::RollbackAnchor;
34
35const SERVING_OWNER_SCHEMA: &str = r#"
36CREATE TABLE chio_serving_owner (
37 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
38 store_uuid TEXT UNIQUE NOT NULL,
39 database_path TEXT NOT NULL,
40 database_device INTEGER NOT NULL CHECK (database_device >= 0),
41 database_inode INTEGER NOT NULL CHECK (database_inode >= 0),
42 lock_root TEXT NOT NULL,
43 lock_device INTEGER NOT NULL CHECK (lock_device >= 0),
44 lock_inode INTEGER NOT NULL CHECK (lock_inode >= 0),
45 owner_epoch INTEGER NOT NULL DEFAULT 0 CHECK (owner_epoch >= 0),
46 lease_id TEXT,
47 opened_at_ms INTEGER
48);
49"#;
50
51struct FixedAuthorityIds {
52 store_uuid: String,
53 lease_ids: VecDeque<String>,
54}
55
56thread_local! {
57 static FIXED_AUTHORITY_IDS: RefCell<Option<FixedAuthorityIds>> = const { RefCell::new(None) };
58}
59
60pub struct FixedAuthorityIdScope {
61 previous: Option<FixedAuthorityIds>,
62 _not_send: std::marker::PhantomData<std::rc::Rc<()>>,
63}
64
65impl Drop for FixedAuthorityIdScope {
66 fn drop(&mut self) {
67 let previous = self.previous.take();
68 FIXED_AUTHORITY_IDS.with(|slot| {
69 *slot.borrow_mut() = previous;
70 });
71 }
72}
73
74pub fn scope_fixed_authority_ids_for_current_thread(
75 store_uuid: impl Into<String>,
76 lease_ids: impl IntoIterator<Item = String>,
77) -> Result<FixedAuthorityIdScope, SqliteServingOwnerError> {
78 let store_uuid = store_uuid.into();
79 validate_uuid_v7(&store_uuid, "fixed authority store UUID")?;
80 let lease_ids = lease_ids
81 .into_iter()
82 .map(|lease_id| validate_uuid_v7(&lease_id, "fixed authority lease ID").map(|_| lease_id))
83 .collect::<Result<VecDeque<_>, _>>()?;
84 let previous = FIXED_AUTHORITY_IDS.with(|slot| {
85 slot.replace(Some(FixedAuthorityIds {
86 store_uuid,
87 lease_ids,
88 }))
89 });
90 Ok(FixedAuthorityIdScope {
91 previous,
92 _not_send: std::marker::PhantomData,
93 })
94}
95
96fn next_store_uuid() -> String {
97 FIXED_AUTHORITY_IDS.with(|slot| {
98 slot.borrow()
99 .as_ref()
100 .map(|ids| ids.store_uuid.clone())
101 .unwrap_or_else(|| uuid::Uuid::now_v7().to_string())
102 })
103}
104
105fn next_lease_id() -> Result<String, SqliteServingOwnerError> {
106 FIXED_AUTHORITY_IDS.with(|slot| {
107 let mut ids = slot.borrow_mut();
108 match ids.as_mut() {
109 Some(ids) => ids.lease_ids.pop_front().ok_or_else(|| {
110 SqliteServingOwnerError::Invalid(
111 "fixed authority lease ID set is exhausted".to_string(),
112 )
113 }),
114 None => Ok(uuid::Uuid::now_v7().to_string()),
115 }
116 })
117}
118
119type SchemaCatalogEntry = (String, String, String, Option<String>);
120
121#[derive(Debug, thiserror::Error)]
122pub enum SqliteServingOwnerError {
123 #[error("sqlite error: {0}")]
124 Sqlite(#[from] rusqlite::Error),
125 #[error("filesystem error: {0}")]
126 Io(#[from] std::io::Error),
127 #[error("sqlite authority store is not provisioned: {0}")]
128 NotProvisioned(String),
129 #[error("sqlite authority store is partially provisioned: {0}")]
130 PartialProvision(String),
131 #[error("sqlite authority store is already serving: {0}")]
132 AlreadyServing(String),
133 #[error("invalid sqlite authority store: {0}")]
134 Invalid(String),
135 #[error("sqlite authority durable outcome is unknown: {0}")]
136 OutcomeUnknown(String),
137}
138
139pub(crate) struct SqliteServingOwner {
140 rollback_anchor: RollbackAnchor,
141 pub(crate) fence: StoreMutationFence,
142 poisoned: AtomicBool,
143 expected_data_version: AtomicU64,
144}
145
146impl SqliteServingOwner {
147 pub(crate) fn outcome_unknown(&self, detail: impl Into<String>) -> SqliteServingOwnerError {
148 self.poisoned.store(true, Ordering::Release);
149 SqliteServingOwnerError::OutcomeUnknown(detail.into())
150 }
151
152 pub(crate) fn verify_authority_anchor(
153 &self,
154 connection: &Connection,
155 ) -> Result<(), SqliteServingOwnerError> {
156 if self.poisoned.load(Ordering::Acquire) {
157 return Err(SqliteServingOwnerError::OutcomeUnknown(
158 "sqlite authority owner is poisoned after an outcome-unknown anchor sync"
159 .to_string(),
160 ));
161 }
162 let actual = authority_data_version(connection)?;
163 if actual != self.expected_data_version.load(Ordering::Acquire) {
164 self.poisoned.store(true, Ordering::Release);
165 return Err(SqliteServingOwnerError::OutcomeUnknown(
166 "authority database changed outside its serving-owner connection".to_string(),
167 ));
168 }
169 self.rollback_anchor.verify_current(connection)
170 }
171
172 pub(crate) fn append_global_commit(
173 &self,
174 transaction: &Transaction<'_>,
175 mutation_kind: &str,
176 projection_kind: &str,
177 projection_key: &str,
178 projection_sequence: u64,
179 ) -> Result<(), SqliteServingOwnerError> {
180 append_global_commit(
181 transaction,
182 mutation_kind,
183 projection_kind,
184 projection_key,
185 projection_sequence,
186 &self.fence,
187 )
188 }
189
190 pub(crate) fn sync_authority_anchor(
191 &self,
192 connection: &Connection,
193 ) -> Result<(), SqliteServingOwnerError> {
194 if self.poisoned.load(Ordering::Acquire) {
195 return Err(SqliteServingOwnerError::OutcomeUnknown(
196 "sqlite authority owner is poisoned after an outcome-unknown anchor sync"
197 .to_string(),
198 ));
199 }
200 if authority_data_version(connection)? != self.expected_data_version.load(Ordering::Acquire)
201 {
202 self.poisoned.store(true, Ordering::Release);
203 return Err(SqliteServingOwnerError::OutcomeUnknown(
204 "authority database changed outside its serving-owner connection".to_string(),
205 ));
206 }
207 if let Err(error) = self.rollback_anchor.sync_after_commit(connection) {
208 self.poisoned.store(true, Ordering::Release);
209 return Err(SqliteServingOwnerError::OutcomeUnknown(error.to_string()));
210 }
211 Ok(())
212 }
213}
214
215pub struct SqliteAuthorityStore {
216 connection: Arc<Mutex<Connection>>,
217 owner: Arc<SqliteServingOwner>,
218}
219
220struct ProvisioningRecord {
221 store_uuid: String,
222 database_path: String,
223 database_device: u64,
224 database_inode: u64,
225 lock_root: String,
226 lock_device: u64,
227 lock_inode: u64,
228 owner_epoch: u64,
229}
230
231impl SqliteAuthorityStore {
232 pub fn ensure_serving_supported() -> Result<(), SqliteServingOwnerError> {
235 #[cfg(unix)]
236 {
237 Ok(())
238 }
239 #[cfg(not(unix))]
240 {
241 Err(SqliteServingOwnerError::Invalid(
242 UNSUPPORTED_SERVING_PLATFORM_MESSAGE.to_string(),
243 ))
244 }
245 }
246
247 pub fn provision(
248 database_path: impl AsRef<Path>,
249 lock_root: impl AsRef<Path>,
250 ) -> Result<(), SqliteServingOwnerError> {
251 Self::ensure_serving_supported()?;
252 let database_path = database_path.as_ref();
253 let lock_root = canonical_lock_root(lock_root.as_ref())?;
254 let provision_lock = File::open(&lock_root)?;
255 provision_lock.lock()?;
256 if let Some(parent) = crate::sqlite_parent_dir_to_create(database_path) {
257 fs::create_dir_all(parent)?;
258 }
259 let authority_parent = database_parent(database_path);
260 validate_secure_directory(authority_parent, "authority database parent")?;
261
262 if !database_path.exists() {
263 let database_file = create_database_file(database_path)?;
264 validate_database_metadata(&database_file.metadata()?)?;
265 database_file.sync_all()?;
266 File::open(authority_parent)?.sync_all()?;
267 }
268 validate_database_path_component(database_path)?;
269 let expected_database = fs::metadata(database_path)?;
270
271 let canonical_database_path = fs::canonicalize(database_path)?;
272 let mut connection = open_existing_database(&canonical_database_path)?;
273 validate_database_identity(&canonical_database_path, &expected_database)?;
274 if owner_table_exists(&connection)? {
275 verify_serving_owner_schema(&connection)?;
276 let record = load_provisioning_record(&connection)?.ok_or_else(|| {
277 SqliteServingOwnerError::PartialProvision(
278 canonical_database_path.display().to_string(),
279 )
280 })?;
281 validate_provisioning_record(&canonical_database_path, &lock_root, &record)?;
282 path_identity::inspect(
283 &lock_root,
284 &canonical_database_path,
285 Some(&record.store_uuid),
286 )?;
287 let lock_path = lock_root.join(format!("{}.lock", record.store_uuid));
288 let lock_file = open_lock_file(&lock_path)?;
289 validate_open_lock_file(&lock_root, &lock_file, &record)?;
290 acquire_serving_lock(&lock_file, &canonical_database_path)?;
291 validate_open_lock_file(&lock_root, &lock_file, &record)?;
292 validate_provisioning_record(&canonical_database_path, &lock_root, &record)?;
293 initialize_offline_authority_schemas(&mut connection)?;
294 initialize_serving_lease_schema(&connection)?;
295 crate::admission_operation_store::initialize_admission_operation_schema(
296 &mut connection,
297 )?;
298 crate::channel_lifecycle_store::initialize_channel_lifecycle_schema(&mut connection)?;
299 crate::channel_release_publisher_store::initialize_channel_release_publisher_schema(
300 &mut connection,
301 )?;
302 crate::tool_outcome_store::initialize_tool_outcome_schema(&mut connection)
303 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
304 initialize_global_commit_schema(&connection)?;
305 seed_global_baseline(&mut connection)?;
306 reset_derived_budget_ack_cache(&connection)?;
307 verify_authority_store_invariants(&connection)?;
308 validate_database_identity(&canonical_database_path, &expected_database)?;
309 connection.execute_batch("PRAGMA wal_checkpoint(FULL);")?;
310 File::open(&canonical_database_path)?.sync_all()?;
311 File::open(database_parent(&canonical_database_path))?.sync_all()?;
312 validate_database_identity(&canonical_database_path, &expected_database)?;
313 let rollback_anchor = RollbackAnchor::new(
314 lock_file,
315 &lock_root,
316 &record.store_uuid,
317 record.lock_device,
318 record.lock_inode,
319 )?;
320 rollback_anchor.migrate_offline(&connection)?;
321 validate_database_identity(&canonical_database_path, &expected_database)?;
322 path_identity::ensure(&lock_root, &canonical_database_path, &record.store_uuid)?;
323 return Ok(());
324 }
325 verify_path_available_for_new_identity(&lock_root, &canonical_database_path)?;
326 initialize_offline_authority_schemas(&mut connection)?;
327 validate_database_identity(&canonical_database_path, &expected_database)?;
328 let database_path = canonical_database_path;
329 for (key, supported) in [
330 ("budget", BUDGET_STORE_SUPPORTED_SCHEMA_VERSION),
331 ("revocation", REVOCATION_STORE_SUPPORTED_SCHEMA_VERSION),
332 (
333 "admission_operation",
334 crate::admission_operation_store::ADMISSION_OPERATION_SUPPORTED_SCHEMA_VERSION,
335 ),
336 (
337 "tool_outcome",
338 crate::tool_outcome_store::TOOL_OUTCOME_SUPPORTED_SCHEMA_VERSION,
339 ),
340 (
341 "frost",
342 crate::frost_store::FROST_STORE_SUPPORTED_SCHEMA_VERSION,
343 ),
344 (
345 "economic_state_cache",
346 crate::economic_state_cache::ECONOMIC_STATE_CACHE_SUPPORTED_SCHEMA_VERSION,
347 ),
348 (
349 "fiscal",
350 crate::fiscal_store::FISCAL_STORE_SUPPORTED_SCHEMA_VERSION,
351 ),
352 (
353 "channel_lifecycle",
354 crate::channel_lifecycle_store::CHANNEL_LIFECYCLE_SUPPORTED_SCHEMA_VERSION,
355 ),
356 (
357 "channel_release_publisher",
358 crate::channel_release_publisher_store::CHANNEL_RELEASE_PUBLISHER_SUPPORTED_SCHEMA_VERSION,
359 ),
360 ] {
361 crate::check_schema_version(
362 &connection,
363 key,
364 supported,
365 &["capability_grant_budgets", "revoked_capabilities"],
366 )
367 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
368 }
369 connection.execute_batch("PRAGMA foreign_keys = ON;")?;
370 verify_pristine_authority_tables(&connection)?;
373
374 let store_uuid = next_store_uuid();
375 let lock_path = lock_root.join(format!("{store_uuid}.lock"));
376 let mut lock_file_created = false;
381 let provision_owner = (|| -> Result<(File, fs::Metadata), SqliteServingOwnerError> {
382 let lock_file = create_lock_file(&lock_path)?;
383 lock_file_created = true;
384 let lock_metadata = lock_file.metadata()?;
385 validate_lock_metadata(&lock_root, &lock_metadata)?;
386 lock_file.sync_all()?;
387 File::open(&lock_root)?.sync_all()?;
388 validate_database_identity(&database_path, &expected_database)?;
389 let database_metadata = fs::metadata(&database_path)?;
390 validate_database_metadata(&database_metadata)?;
391 let transaction =
392 connection.transaction_with_behavior(TransactionBehavior::Immediate)?;
393 transaction.execute_batch(SERVING_OWNER_SCHEMA)?;
394 let changed = transaction.execute(
395 r#"
396 INSERT INTO chio_serving_owner (
397 singleton, store_uuid, database_path,
398 database_device, database_inode, lock_root,
399 lock_device, lock_inode, owner_epoch
400 ) VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, 0)
401 "#,
402 params![
403 &store_uuid,
404 path_text(&database_path)?,
405 metadata_device(&database_metadata)?,
406 metadata_inode(&database_metadata)?,
407 path_text(&lock_root)?,
408 metadata_device(&lock_metadata)?,
409 metadata_inode(&lock_metadata)?,
410 ],
411 )?;
412 if changed != 1 {
413 return Err(SqliteServingOwnerError::Invalid(
414 "serving-owner insert did not affect exactly one row".to_string(),
415 ));
416 }
417 transaction.commit().map_err(|error| {
418 SqliteServingOwnerError::OutcomeUnknown(format!(
419 "sqlite authority provisioning commit outcome is unknown: {error}"
420 ))
421 })?;
422 Ok((lock_file, lock_metadata))
423 })();
424 let (lock_file, lock_metadata) = match provision_owner {
425 Ok(artifacts) => artifacts,
426 Err(error) => {
427 if !matches!(error, SqliteServingOwnerError::OutcomeUnknown(_)) && lock_file_created
429 {
430 let _ = fs::remove_file(&lock_path);
431 let _ = File::open(&lock_root).and_then(|directory| directory.sync_all());
432 }
433 return Err(error);
434 }
435 };
436 verify_serving_owner_schema(&connection)?;
437 initialize_serving_lease_schema(&connection)?;
438 crate::admission_operation_store::initialize_admission_operation_schema(&mut connection)?;
439 crate::channel_lifecycle_store::initialize_channel_lifecycle_schema(&mut connection)?;
440 crate::channel_release_publisher_store::initialize_channel_release_publisher_schema(
441 &mut connection,
442 )?;
443 crate::tool_outcome_store::initialize_tool_outcome_schema(&mut connection)
444 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
445 initialize_global_commit_schema(&connection)?;
446 seed_global_baseline(&mut connection)?;
447 reset_derived_budget_ack_cache(&connection)?;
448 verify_authority_store_invariants(&connection)?;
449 validate_database_identity(&database_path, &expected_database)?;
450 File::open(&database_path)?.sync_all()?;
451 File::open(&lock_root)?.sync_all()?;
452 if let Some(parent) = database_path.parent() {
453 File::open(parent)?.sync_all()?;
454 }
455 validate_database_identity(&database_path, &expected_database)?;
456 let rollback_anchor = RollbackAnchor::new(
457 lock_file,
458 &lock_root,
459 &store_uuid,
460 read_u64(metadata_device(&lock_metadata)?, "lock_device")?,
461 read_u64(metadata_inode(&lock_metadata)?, "lock_inode")?,
462 )?;
463 rollback_anchor.seed_new(&connection)?;
464 validate_database_identity(&database_path, &expected_database)?;
465 path_identity::ensure(&lock_root, &database_path, &store_uuid)?;
466 Ok(())
467 }
468
469 pub fn open_serving(
470 database_path: impl AsRef<Path>,
471 lock_root: impl AsRef<Path>,
472 ) -> Result<Self, SqliteServingOwnerError> {
473 Self::ensure_serving_supported()?;
474 let database_path = database_path.as_ref();
475 validate_database_path_component(database_path)?;
476 let database_path = fs::canonicalize(database_path)?;
477 let lock_root = canonical_lock_root(lock_root.as_ref())?;
478 let open_lock = File::open(&lock_root)?;
479 open_lock.lock()?;
480 validate_secure_directory(database_parent(&database_path), "authority database parent")?;
481 let expected_database = fs::metadata(&database_path)?;
482 let mut connection = open_existing_database(&database_path)?;
483 let expected_data_version = authority_data_version(&connection)?;
484 validate_database_identity(&database_path, &expected_database)?;
485 if !owner_table_exists(&connection)? {
486 verify_path_available_for_new_identity(&lock_root, &database_path)?;
487 return Err(SqliteServingOwnerError::NotProvisioned(path_text(
488 &database_path,
489 )?));
490 }
491 verify_serving_owner_schema(&connection)?;
492 let record = load_provisioning_record(&connection)?.ok_or_else(|| {
493 SqliteServingOwnerError::PartialProvision(database_path.display().to_string())
494 })?;
495 validate_provisioning_record(&database_path, &lock_root, &record)?;
496 path_identity::inspect(&lock_root, &database_path, Some(&record.store_uuid))?;
497 let lock_path = lock_root.join(format!("{}.lock", record.store_uuid));
498 let lock_file = open_lock_file(&lock_path)?;
499 validate_open_lock_file(&lock_root, &lock_file, &record)?;
500 acquire_serving_lock(&lock_file, &database_path)?;
501 validate_open_lock_file(&lock_root, &lock_file, &record)?;
502 validate_provisioning_record(&database_path, &lock_root, &record)?;
503
504 connection.execute_batch(
505 r#"
506 PRAGMA journal_mode = WAL;
507 PRAGMA synchronous = FULL;
508 PRAGMA busy_timeout = 5000;
509 PRAGMA foreign_keys = ON;
510 "#,
511 )?;
512 initialize_serving_lease_schema(&connection)?;
513 crate::admission_operation_store::initialize_admission_operation_schema(&mut connection)?;
514 crate::channel_lifecycle_store::initialize_channel_lifecycle_schema(&mut connection)?;
515 crate::channel_release_publisher_store::initialize_channel_release_publisher_schema(
516 &mut connection,
517 )?;
518 crate::tool_outcome_store::initialize_tool_outcome_schema(&mut connection)
519 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
520 verify_global_commit_schema(&connection)?;
521 reset_derived_budget_ack_cache(&connection)?;
522 for (key, supported) in [
523 ("budget", BUDGET_STORE_SUPPORTED_SCHEMA_VERSION),
524 ("revocation", REVOCATION_STORE_SUPPORTED_SCHEMA_VERSION),
525 (
526 "admission_operation",
527 crate::admission_operation_store::ADMISSION_OPERATION_SUPPORTED_SCHEMA_VERSION,
528 ),
529 (
530 "tool_outcome",
531 crate::tool_outcome_store::TOOL_OUTCOME_SUPPORTED_SCHEMA_VERSION,
532 ),
533 (
534 "frost",
535 crate::frost_store::FROST_STORE_SUPPORTED_SCHEMA_VERSION,
536 ),
537 (
538 "economic_state_cache",
539 crate::economic_state_cache::ECONOMIC_STATE_CACHE_SUPPORTED_SCHEMA_VERSION,
540 ),
541 (
542 "fiscal",
543 crate::fiscal_store::FISCAL_STORE_SUPPORTED_SCHEMA_VERSION,
544 ),
545 (
546 "channel_lifecycle",
547 crate::channel_lifecycle_store::CHANNEL_LIFECYCLE_SUPPORTED_SCHEMA_VERSION,
548 ),
549 (
550 "channel_release_publisher",
551 crate::channel_release_publisher_store::CHANNEL_RELEASE_PUBLISHER_SUPPORTED_SCHEMA_VERSION,
552 ),
553 ] {
554 crate::check_schema_version(
555 &connection,
556 key,
557 supported,
558 &["capability_grant_budgets", "revoked_capabilities"],
559 )
560 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
561 }
562 verify_authority_store_invariants(&connection)?;
563 let rollback_anchor = RollbackAnchor::new(
564 lock_file,
565 &lock_root,
566 &record.store_uuid,
567 record.lock_device,
568 record.lock_inode,
569 )?;
570 rollback_anchor.reconcile_startup(&connection)?;
571 validate_database_identity(&database_path, &expected_database)?;
572 path_identity::ensure(&lock_root, &database_path, &record.store_uuid)?;
573
574 let owner_epoch = record.owner_epoch.checked_add(1).ok_or_else(|| {
575 SqliteServingOwnerError::Invalid("serving owner epoch overflowed u64".to_string())
576 })?;
577 let lease_id = next_lease_id()?;
578 let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?;
579 let current = load_provisioning_record_tx(&transaction)?.ok_or_else(|| {
580 SqliteServingOwnerError::NotProvisioned(database_path.display().to_string())
581 })?;
582 if current.store_uuid != record.store_uuid || current.owner_epoch != record.owner_epoch {
583 return Err(SqliteServingOwnerError::AlreadyServing(
584 "serving owner changed while acquiring the lock".to_string(),
585 ));
586 }
587 verify_authority_store_invariants(&transaction)?;
588 let authority_head = transaction.query_row(
589 "SELECT head_index FROM admission_authority_meta WHERE singleton = 1",
590 [],
591 |row| row.get::<_, i64>(0),
592 )?;
593 if authority_head <= 0 {
594 return Err(SqliteServingOwnerError::Invalid(
595 "admission authority head is not positive".to_string(),
596 ));
597 }
598 if record.owner_epoch > 0 {
599 let previous_lease_id = transaction
600 .query_row(
601 "SELECT lease_id FROM chio_serving_owner WHERE singleton = 1",
602 [],
603 |row| row.get::<_, Option<String>>(0),
604 )?
605 .ok_or_else(|| {
606 SqliteServingOwnerError::Invalid(
607 "active serving owner lost its lease identity".to_string(),
608 )
609 })?;
610 let closed = transaction.execute(
611 r#"
612 UPDATE chio_serving_leases
613 SET end_head_index = ?1
614 WHERE store_uuid = ?2 AND owner_epoch = ?3
615 AND lease_id = ?4 AND end_head_index IS NULL
616 "#,
617 params![
618 authority_head,
619 &record.store_uuid,
620 sqlite_u64(record.owner_epoch, "owner_epoch")?,
621 previous_lease_id,
622 ],
623 )?;
624 if closed != 1 {
625 return Err(SqliteServingOwnerError::Invalid(
626 "previous serving lease was not open exactly once".to_string(),
627 ));
628 }
629 }
630 let opened_at_ms = now_ms()?;
631 let changed = transaction.execute(
632 r#"
633 UPDATE chio_serving_owner
634 SET owner_epoch = ?1, lease_id = ?2, opened_at_ms = ?3
635 WHERE singleton = 1 AND owner_epoch = ?4
636 "#,
637 params![
638 sqlite_u64(owner_epoch, "owner_epoch")?,
639 &lease_id,
640 opened_at_ms,
641 sqlite_u64(record.owner_epoch, "owner_epoch")?,
642 ],
643 )?;
644 if changed != 1 {
645 return Err(SqliteServingOwnerError::AlreadyServing(
646 "serving owner changed while advancing its epoch".to_string(),
647 ));
648 }
649 let inserted = transaction.execute(
650 r#"
651 INSERT INTO chio_serving_leases (
652 store_uuid, owner_epoch, lease_id,
653 start_head_index, end_head_index, opened_at_ms
654 ) VALUES (?1, ?2, ?3, ?4, NULL, ?5)
655 "#,
656 params![
657 &record.store_uuid,
658 sqlite_u64(owner_epoch, "owner_epoch")?,
659 &lease_id,
660 authority_head,
661 opened_at_ms,
662 ],
663 )?;
664 if inserted != 1 {
665 return Err(SqliteServingOwnerError::Invalid(
666 "serving lease insert did not affect exactly one row".to_string(),
667 ));
668 }
669 verify_authority_store_invariants(&transaction)?;
670 transaction.commit().map_err(|error| {
671 SqliteServingOwnerError::OutcomeUnknown(format!(
672 "sqlite serving-owner epoch commit outcome is unknown: {error}"
673 ))
674 })?;
675 rollback_anchor.sync_after_commit(&connection)?;
676 if authority_data_version(&connection)? != expected_data_version {
677 return Err(SqliteServingOwnerError::Invalid(
678 "authority database changed concurrently while opening".to_string(),
679 ));
680 }
681 let owner = Arc::new(SqliteServingOwner {
682 rollback_anchor,
683 fence: StoreMutationFence {
684 store_uuid: record.store_uuid,
685 lease_id,
686 owner_epoch,
687 },
688 poisoned: AtomicBool::new(false),
689 expected_data_version: AtomicU64::new(expected_data_version),
690 });
691 crate::channel_release_publisher_store::quarantine_incomplete_dispatches_at_startup(
692 &mut connection,
693 &owner,
694 )?;
695 Ok(Self {
696 connection: Arc::new(Mutex::new(connection)),
697 owner,
698 })
699 }
700
701 #[must_use]
702 pub fn mutation_fence(&self) -> StoreMutationFence {
703 self.owner.fence.clone()
704 }
705
706 #[must_use]
707 pub fn budget_store(&self) -> SqliteBudgetStore {
708 SqliteBudgetStore::open_alongside(self.connection.clone(), self.owner.clone())
709 }
710
711 #[must_use]
712 pub fn revocation_store(&self) -> SqliteRevocationStore {
713 SqliteRevocationStore::open_alongside(self.connection.clone(), self.owner.clone())
714 }
715
716 #[must_use]
717 pub fn admission_operation_store(
718 &self,
719 ) -> crate::admission_operation_store::SqliteAdmissionOperationStore {
720 crate::admission_operation_store::SqliteAdmissionOperationStore::open_alongside(
721 self.connection.clone(),
722 self.owner.clone(),
723 )
724 }
725
726 #[must_use]
727 pub fn tool_outcome_store(&self) -> crate::tool_outcome_store::SqliteToolOutcomeStore {
728 crate::tool_outcome_store::SqliteToolOutcomeStore::open_alongside(
729 self.connection.clone(),
730 self.owner.clone(),
731 )
732 }
733
734 #[must_use]
735 pub fn frost_store(&self) -> crate::frost_store::SqliteFrostStore {
736 crate::frost_store::SqliteFrostStore::open_alongside(
737 self.connection.clone(),
738 self.owner.clone(),
739 )
740 }
741
742 #[must_use]
743 pub fn economic_state_cache(&self) -> crate::economic_state_cache::SqliteEconomicStateCache {
744 crate::economic_state_cache::SqliteEconomicStateCache::open_alongside(
745 self.connection.clone(),
746 self.owner.clone(),
747 )
748 }
749
750 #[must_use]
751 pub fn fiscal_store(&self) -> crate::fiscal_store::SqliteFiscalStore {
752 crate::fiscal_store::SqliteFiscalStore::open_alongside(
753 self.connection.clone(),
754 self.owner.clone(),
755 )
756 }
757
758 #[must_use]
759 pub fn channel_lifecycle_store(
760 &self,
761 ) -> crate::channel_lifecycle_store::SqliteChannelLifecycleStore {
762 crate::channel_lifecycle_store::SqliteChannelLifecycleStore::open_alongside(
763 self.connection.clone(),
764 self.owner.clone(),
765 )
766 }
767
768 #[must_use]
769 pub fn channel_release_publisher_store(
770 &self,
771 ) -> crate::channel_release_publisher_store::SqliteChannelReleasePublisherStore {
772 crate::channel_release_publisher_store::SqliteChannelReleasePublisherStore::open_alongside(
773 self.connection.clone(),
774 self.owner.clone(),
775 )
776 }
777}
778
779fn initialize_offline_authority_schemas(
780 connection: &mut Connection,
781) -> Result<(), SqliteServingOwnerError> {
782 let has_revocation_schema = connection.query_row(
783 r#"
784 SELECT EXISTS(
785 SELECT 1 FROM sqlite_master
786 WHERE type = 'table' AND name = 'revoked_capabilities'
787 )
788 "#,
789 [],
790 |row| row.get::<_, bool>(0),
791 )?;
792 if has_revocation_schema {
793 crate::check_schema_version(
794 connection,
795 "revocation",
796 REVOCATION_STORE_SUPPORTED_SCHEMA_VERSION,
797 &["revoked_capabilities"],
798 )
799 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
800 }
801 SqliteBudgetStore::initialize_connection_offline(connection).map_err(|error| {
802 SqliteServingOwnerError::Invalid(format!("failed to initialize budget schema: {error}"))
803 })?;
804 initialize_revocation_schema(connection, true).map_err(|error| {
805 SqliteServingOwnerError::Invalid(format!("failed to initialize revocation schema: {error}"))
806 })?;
807 crate::stamp_schema_version(
808 connection,
809 "revocation",
810 REVOCATION_STORE_SUPPORTED_SCHEMA_VERSION,
811 )
812 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
813 crate::frost_store::initialize_frost_schema(connection)
814 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
815 crate::economic_state_cache::initialize_economic_state_cache_schema(connection)
816 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
817 crate::fiscal_store::initialize_fiscal_schema(connection)
818 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
819 Ok(())
820}
821
822pub(crate) fn verify_budget_fence(
823 transaction: &Transaction<'_>,
824 owner: Option<&SqliteServingOwner>,
825) -> Result<(), BudgetStoreError> {
826 let current = load_fence_tx(transaction).map_err(BudgetStoreError::from)?;
827 match (owner, current) {
828 (None, FenceState::Unprovisioned) => Ok(()),
829 (Some(owner), FenceState::Active(current)) if owner.fence == current => Ok(()),
830 (Some(owner), current) => Err(BudgetStoreError::Fenced {
831 expected_epoch: owner.fence.owner_epoch,
832 actual_epoch: current.owner_epoch(),
833 }),
834 (None, current) => Err(BudgetStoreError::Fenced {
835 expected_epoch: 0,
836 actual_epoch: current.owner_epoch(),
837 }),
838 }
839}
840
841pub(crate) fn verify_revocation_fence(
842 transaction: &Transaction<'_>,
843 owner: Option<&SqliteServingOwner>,
844) -> Result<(), RevocationStoreError> {
845 verify_budget_fence(transaction, owner).map_err(|error| match error {
846 BudgetStoreError::Fenced {
847 expected_epoch,
848 actual_epoch,
849 } => RevocationStoreError::Fenced {
850 expected_epoch,
851 actual_epoch,
852 },
853 error => RevocationStoreError::Sync(error.to_string()),
854 })
855}
856
857fn owner_table_exists(connection: &Connection) -> Result<bool, rusqlite::Error> {
858 connection.query_row(
859 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'chio_serving_owner')",
860 [],
861 |row| row.get(0),
862 )
863}
864
865fn verify_path_available_for_new_identity(
866 lock_root: &Path,
867 canonical_database_path: &Path,
868) -> Result<(), SqliteServingOwnerError> {
869 if path_identity::inspect(lock_root, canonical_database_path, None)?
870 == path_identity::MarkerStatus::Present
871 {
872 return Err(SqliteServingOwnerError::Invalid(format!(
873 "local path identity continuity marker remains for `{}` but the database lost its provisioning record; refusing to mint a replacement store UUID (this marker is local continuity evidence, not independent rollback protection)",
874 canonical_database_path.display()
875 )));
876 }
877 Ok(())
878}
879
880fn verify_serving_owner_schema(connection: &Connection) -> Result<(), SqliteServingOwnerError> {
881 let expected = Connection::open_in_memory()?;
882 expected.execute_batch(SERVING_OWNER_SCHEMA)?;
883 if serving_owner_schema_catalog(connection)? != serving_owner_schema_catalog(&expected)? {
884 return Err(SqliteServingOwnerError::Invalid(
885 "serving owner schema differs from the canonical definition".to_string(),
886 ));
887 }
888 Ok(())
889}
890
891fn serving_owner_schema_catalog(
892 connection: &Connection,
893) -> Result<Vec<SchemaCatalogEntry>, SqliteServingOwnerError> {
894 let mut statement = connection.prepare(
895 r#"
896 SELECT type, name, tbl_name, sql FROM sqlite_schema
897 WHERE name = 'chio_serving_owner' OR tbl_name = 'chio_serving_owner'
898 ORDER BY type, name, tbl_name
899 "#,
900 )?;
901 let catalog = statement
902 .query_map([], |row| {
903 Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
904 })?
905 .collect::<Result<Vec<_>, _>>()?;
906 Ok(catalog)
907}
908
909pub(crate) fn verify_historical_revocation_commit(
910 connection: &Connection,
911 metadata: &RevocationCommitMetadata,
912) -> Result<(), BudgetStoreError> {
913 metadata.validate()?;
914 if metadata.guarantee_level != BudgetGuaranteeLevel::SingleNodeAtomic {
915 return Err(BudgetStoreError::Invariant(
916 "local revocation provenance requires a single-node authority".to_string(),
917 ));
918 }
919 verify_historical_budget_authority(connection, &metadata.authority)?;
920 let lease_epoch = i64::try_from(metadata.authority.lease_epoch).map_err(|_| {
921 BudgetStoreError::Invariant("revocation lease epoch exceeds SQLite range".to_string())
922 })?;
923 let commit_index = i64::try_from(metadata.commit_index).map_err(|_| {
924 BudgetStoreError::Invariant("revocation commit index exceeds SQLite range".to_string())
925 })?;
926 let valid = connection.query_row(
927 r#"
928 SELECT EXISTS (
929 SELECT 1
930 FROM chio_serving_leases AS lease
931 JOIN admission_authority_commits AS committed
932 ON committed.commit_index = ?4
933 JOIN admission_authority_meta AS authority ON authority.singleton = 1
934 WHERE lease.store_uuid = ?1
935 AND lease.owner_epoch = ?2
936 AND lease.lease_id = ?3
937 AND ?4 >= lease.start_head_index
938 AND (
939 (lease.end_head_index IS NOT NULL
940 AND ?4 <= lease.end_head_index)
941 OR
942 (lease.end_head_index IS NULL
943 AND ?4 <= authority.head_index)
944 )
945 )
946 "#,
947 params![
948 &metadata.authority.authority_id,
949 lease_epoch,
950 &metadata.authority.lease_id,
951 commit_index,
952 ],
953 |row| row.get::<_, bool>(0),
954 )?;
955 if !valid {
956 return Err(BudgetStoreError::Invariant(
957 "revocation commit is outside its durable serving lease".to_string(),
958 ));
959 }
960 Ok(())
961}
962
963pub(crate) fn verify_historical_budget_authority(
964 connection: &Connection,
965 authority: &BudgetEventAuthority,
966) -> Result<(), BudgetStoreError> {
967 if authority.authority_id.is_empty()
968 || authority.lease_id.is_empty()
969 || authority.lease_epoch == 0
970 {
971 return Err(BudgetStoreError::Invariant(
972 "budget authority requires a durable serving lease".to_string(),
973 ));
974 }
975 let lease_epoch = i64::try_from(authority.lease_epoch).map_err(|_| {
976 BudgetStoreError::Invariant("budget authority epoch exceeds SQLite range".to_string())
977 })?;
978 let valid = connection.query_row(
979 r#"
980 SELECT EXISTS (
981 SELECT 1
982 FROM chio_serving_leases AS lease
983 JOIN chio_serving_owner AS owner ON owner.singleton = 1
984 WHERE lease.store_uuid = ?1
985 AND lease.owner_epoch = ?2
986 AND lease.lease_id = ?3
987 AND (
988 lease.end_head_index IS NOT NULL
989 OR
990 (owner.store_uuid = lease.store_uuid
991 AND owner.owner_epoch = lease.owner_epoch
992 AND owner.lease_id = lease.lease_id)
993 )
994 )
995 "#,
996 params![&authority.authority_id, lease_epoch, &authority.lease_id,],
997 |row| row.get::<_, bool>(0),
998 )?;
999 if !valid {
1000 return Err(BudgetStoreError::Invariant(
1001 "budget authority is outside durable serving lease history".to_string(),
1002 ));
1003 }
1004 Ok(())
1005}
1006
1007pub(crate) fn provisioned_owner_epoch(
1008 connection: &Connection,
1009) -> Result<Option<u64>, BudgetStoreError> {
1010 if !owner_table_exists(connection)? {
1011 return Ok(None);
1012 }
1013 let epoch = connection
1014 .query_row(
1015 "SELECT owner_epoch FROM chio_serving_owner WHERE singleton = 1",
1016 [],
1017 |row| row.get::<_, i64>(0),
1018 )
1019 .optional()?
1020 .ok_or_else(|| {
1021 BudgetStoreError::Invariant(
1022 "sqlite authority store is partially provisioned".to_string(),
1023 )
1024 })?;
1025 u64::try_from(epoch)
1026 .map(Some)
1027 .map_err(|_| BudgetStoreError::Invariant("negative serving owner epoch".to_string()))
1028}
1029
1030fn load_provisioning_record(
1031 connection: &Connection,
1032) -> Result<Option<ProvisioningRecord>, SqliteServingOwnerError> {
1033 if !owner_table_exists(connection)? {
1034 return Ok(None);
1035 }
1036 load_provisioning_record_query(connection).map_err(Into::into)
1037}
1038
1039fn load_provisioning_record_tx(
1040 transaction: &Transaction<'_>,
1041) -> Result<Option<ProvisioningRecord>, rusqlite::Error> {
1042 load_provisioning_record_query(transaction)
1043}
1044
1045fn load_provisioning_record_query(
1046 connection: &Connection,
1047) -> Result<Option<ProvisioningRecord>, rusqlite::Error> {
1048 connection
1049 .query_row(
1050 r#"
1051 SELECT store_uuid, database_path, database_device, database_inode,
1052 lock_root, lock_device, lock_inode, owner_epoch
1053 FROM chio_serving_owner WHERE singleton = 1
1054 "#,
1055 [],
1056 |row| {
1057 Ok((
1058 row.get::<_, String>(0)?,
1059 row.get::<_, String>(1)?,
1060 row.get::<_, i64>(2)?,
1061 row.get::<_, i64>(3)?,
1062 row.get::<_, String>(4)?,
1063 row.get::<_, i64>(5)?,
1064 row.get::<_, i64>(6)?,
1065 row.get::<_, i64>(7)?,
1066 ))
1067 },
1068 )
1069 .optional()?
1070 .map(|row| {
1071 Ok(ProvisioningRecord {
1072 store_uuid: row.0,
1073 database_path: row.1,
1074 database_device: read_u64(row.2, "database_device")?,
1075 database_inode: read_u64(row.3, "database_inode")?,
1076 lock_root: row.4,
1077 lock_device: read_u64(row.5, "lock_device")?,
1078 lock_inode: read_u64(row.6, "lock_inode")?,
1079 owner_epoch: read_u64(row.7, "owner_epoch")?,
1080 })
1081 })
1082 .transpose()
1083 .map_err(|error: SqliteServingOwnerError| {
1084 rusqlite::Error::InvalidParameterName(error.to_string())
1085 })
1086}
1087
1088enum FenceState {
1089 Unprovisioned,
1090 Inactive(u64),
1091 Active(StoreMutationFence),
1092}
1093
1094impl FenceState {
1095 fn owner_epoch(&self) -> Option<u64> {
1096 match self {
1097 Self::Unprovisioned => None,
1098 Self::Inactive(epoch) => Some(*epoch),
1099 Self::Active(fence) => Some(fence.owner_epoch),
1100 }
1101 }
1102}
1103
1104fn load_fence_tx(transaction: &Transaction<'_>) -> Result<FenceState, rusqlite::Error> {
1105 let table_exists = transaction.query_row(
1106 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'chio_serving_owner')",
1107 [],
1108 |row| row.get::<_, bool>(0),
1109 )?;
1110 if !table_exists {
1111 return Ok(FenceState::Unprovisioned);
1112 }
1113 let row = transaction
1114 .query_row(
1115 "SELECT store_uuid, lease_id, owner_epoch FROM chio_serving_owner WHERE singleton = 1",
1116 [],
1117 |row| {
1118 Ok((
1119 row.get::<_, String>(0)?,
1120 row.get::<_, Option<String>>(1)?,
1121 row.get::<_, i64>(2)?,
1122 ))
1123 },
1124 )
1125 .optional()?;
1126 let Some((store_uuid, lease_id, owner_epoch)) = row else {
1127 return Err(rusqlite::Error::InvalidQuery);
1128 };
1129 let owner_epoch = u64::try_from(owner_epoch).map_err(|_| rusqlite::Error::InvalidQuery)?;
1130 Ok(match lease_id {
1131 Some(lease_id) => FenceState::Active(StoreMutationFence {
1132 store_uuid,
1133 lease_id,
1134 owner_epoch,
1135 }),
1136 None => FenceState::Inactive(owner_epoch),
1137 })
1138}
1139
1140fn validate_provisioning_record(
1141 database_path: &Path,
1142 lock_root: &Path,
1143 record: &ProvisioningRecord,
1144) -> Result<(), SqliteServingOwnerError> {
1145 validate_store_uuid(&record.store_uuid)?;
1146 if record.database_path != path_text(database_path)?
1147 || record.lock_root != path_text(lock_root)?
1148 {
1149 return Err(SqliteServingOwnerError::Invalid(
1150 "provisioned path identity changed".to_string(),
1151 ));
1152 }
1153 let database_metadata = fs::metadata(database_path)?;
1154 validate_database_metadata(&database_metadata)?;
1155 if metadata_device(&database_metadata)? != sqlite_u64(record.database_device, "device")?
1156 || metadata_inode(&database_metadata)? != sqlite_u64(record.database_inode, "inode")?
1157 {
1158 return Err(SqliteServingOwnerError::Invalid(
1159 "database file identity changed".to_string(),
1160 ));
1161 }
1162 let lock_path = lock_root.join(format!("{}.lock", record.store_uuid));
1163 let lock_metadata = fs::symlink_metadata(&lock_path)?;
1164 validate_lock_metadata(lock_root, &lock_metadata)?;
1165 if metadata_device(&lock_metadata)? != sqlite_u64(record.lock_device, "lock_device")?
1166 || metadata_inode(&lock_metadata)? != sqlite_u64(record.lock_inode, "lock_inode")?
1167 {
1168 return Err(SqliteServingOwnerError::Invalid(
1169 "serving lock inode changed".to_string(),
1170 ));
1171 }
1172 Ok(())
1173}
1174
1175fn validate_open_lock_file(
1176 lock_root: &Path,
1177 lock_file: &File,
1178 record: &ProvisioningRecord,
1179) -> Result<(), SqliteServingOwnerError> {
1180 let metadata = lock_file.metadata()?;
1181 validate_lock_metadata(lock_root, &metadata)?;
1182 if metadata_device(&metadata)? != sqlite_u64(record.lock_device, "lock_device")?
1183 || metadata_inode(&metadata)? != sqlite_u64(record.lock_inode, "lock_inode")?
1184 {
1185 return Err(SqliteServingOwnerError::Invalid(
1186 "opened serving lock identity changed".to_string(),
1187 ));
1188 }
1189 Ok(())
1190}
1191
1192fn validate_store_uuid(value: &str) -> Result<(), SqliteServingOwnerError> {
1193 validate_uuid_v7(value, "provisioned store UUID")
1194}
1195
1196fn validate_uuid_v7(value: &str, field: &str) -> Result<(), SqliteServingOwnerError> {
1197 let parsed = uuid::Uuid::parse_str(value).map_err(|_| {
1198 SqliteServingOwnerError::Invalid(format!("{field} is not canonical UUID-v7"))
1199 })?;
1200 if parsed.get_version_num() != 7 || parsed.to_string() != value {
1201 return Err(SqliteServingOwnerError::Invalid(format!(
1202 "{field} is not canonical UUID-v7"
1203 )));
1204 }
1205 Ok(())
1206}
1207
1208fn canonical_lock_root(path: &Path) -> Result<PathBuf, SqliteServingOwnerError> {
1209 let canonical = fs::canonicalize(path)?;
1210 validate_secure_directory(&canonical, "serving lock root")?;
1211 Ok(canonical)
1212}
1213
1214fn validate_secure_directory(
1215 path: &Path,
1216 description: &str,
1217) -> Result<(), SqliteServingOwnerError> {
1218 let metadata = fs::symlink_metadata(path)?;
1219 if !metadata.file_type().is_dir() {
1220 return Err(SqliteServingOwnerError::Invalid(format!(
1221 "{description} is not a directory"
1222 )));
1223 }
1224 #[cfg(unix)]
1225 {
1226 use std::os::unix::fs::MetadataExt;
1227 if metadata.uid() != nix::unistd::geteuid().as_raw() || metadata.mode() & 0o022 != 0 {
1228 return Err(SqliteServingOwnerError::Invalid(format!(
1229 "{description} must be owned by the effective user and not group or world writable"
1230 )));
1231 }
1232 }
1233 Ok(())
1234}
1235
1236fn database_parent(path: &Path) -> &Path {
1237 path.parent()
1238 .filter(|parent| !parent.as_os_str().is_empty())
1239 .unwrap_or_else(|| Path::new("."))
1240}
1241
1242fn create_database_file(path: &Path) -> Result<File, SqliteServingOwnerError> {
1243 let mut options = OpenOptions::new();
1244 options.create_new(true).read(true).write(true);
1245 #[cfg(unix)]
1246 {
1247 use std::os::unix::fs::OpenOptionsExt;
1248 options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
1249 }
1250 options.open(path).map_err(Into::into)
1251}
1252
1253fn open_existing_database(path: &Path) -> Result<Connection, SqliteServingOwnerError> {
1254 let connection = Connection::open_with_flags(
1255 path,
1256 rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE
1257 | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX
1258 | rusqlite::OpenFlags::SQLITE_OPEN_NOFOLLOW,
1259 )?;
1260 connection.execute_batch("PRAGMA busy_timeout = 5000;")?;
1263 Ok(connection)
1264}
1265
1266fn create_lock_file(path: &Path) -> Result<File, SqliteServingOwnerError> {
1267 let mut options = OpenOptions::new();
1268 options.create_new(true).read(true).write(true);
1269 #[cfg(unix)]
1270 {
1271 use std::os::unix::fs::OpenOptionsExt;
1272 options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
1273 }
1274 options.open(path).map_err(Into::into)
1275}
1276
1277fn open_lock_file(path: &Path) -> Result<File, SqliteServingOwnerError> {
1278 let mut options = OpenOptions::new();
1279 options.read(true).write(true);
1280 #[cfg(unix)]
1281 {
1282 use std::os::unix::fs::OpenOptionsExt;
1283 options.custom_flags(libc::O_NOFOLLOW);
1284 }
1285 options.open(path).map_err(Into::into)
1286}
1287
1288fn acquire_serving_lock(
1289 lock_file: &File,
1290 database_path: &Path,
1291) -> Result<(), SqliteServingOwnerError> {
1292 lock_file
1293 .try_lock()
1294 .map_err(|error| classify_lock_error(database_path, error.into()))
1295}
1296
1297fn classify_lock_error(database_path: &Path, error: std::io::Error) -> SqliteServingOwnerError {
1298 if error.kind() == std::io::ErrorKind::WouldBlock {
1299 SqliteServingOwnerError::AlreadyServing(format!("{}: {error}", database_path.display()))
1300 } else {
1301 SqliteServingOwnerError::Io(error)
1302 }
1303}
1304
1305fn validate_database_metadata(metadata: &fs::Metadata) -> Result<(), SqliteServingOwnerError> {
1306 if !metadata.file_type().is_file() {
1307 return Err(SqliteServingOwnerError::Invalid(
1308 "authority database is not a regular file".to_string(),
1309 ));
1310 }
1311 #[cfg(unix)]
1312 {
1313 use std::os::unix::fs::MetadataExt;
1314 if metadata.nlink() != 1
1315 || metadata.uid() != nix::unistd::geteuid().as_raw()
1316 || metadata.mode() & 0o777 != 0o600
1317 {
1318 return Err(SqliteServingOwnerError::Invalid(
1319 "authority database ownership, mode, or link count is invalid".to_string(),
1320 ));
1321 }
1322 }
1323 Ok(())
1324}
1325
1326fn validate_database_path_component(path: &Path) -> Result<(), SqliteServingOwnerError> {
1327 let metadata = fs::symlink_metadata(path)?;
1328 if metadata.file_type().is_symlink() {
1329 return Err(SqliteServingOwnerError::Invalid(
1330 "authority database path must not be a symlink".to_string(),
1331 ));
1332 }
1333 validate_database_metadata(&metadata)
1334}
1335
1336fn validate_database_identity(
1337 path: &Path,
1338 expected: &fs::Metadata,
1339) -> Result<(), SqliteServingOwnerError> {
1340 validate_database_path_component(path)?;
1341 let actual = fs::metadata(path)?;
1342 if metadata_device(&actual)? != metadata_device(expected)?
1343 || metadata_inode(&actual)? != metadata_inode(expected)?
1344 {
1345 return Err(SqliteServingOwnerError::Invalid(
1346 "authority database identity changed while opening".to_string(),
1347 ));
1348 }
1349 Ok(())
1350}
1351
1352#[cfg(not(unix))]
1353const UNSUPPORTED_SERVING_PLATFORM_MESSAGE: &str =
1354 "sqlite authority serving requires Unix file identity and positioned I/O";
1355
1356fn validate_lock_metadata(
1357 lock_root: &Path,
1358 metadata: &fs::Metadata,
1359) -> Result<(), SqliteServingOwnerError> {
1360 if !metadata.file_type().is_file() {
1361 return Err(SqliteServingOwnerError::Invalid(
1362 "serving lock is not a regular file".to_string(),
1363 ));
1364 }
1365 #[cfg(unix)]
1366 {
1367 use std::os::unix::fs::MetadataExt;
1368 let root_metadata = fs::metadata(lock_root)?;
1369 if metadata.nlink() != 1
1370 || metadata.mode() & 0o777 != 0o600
1371 || metadata.uid() != nix::unistd::geteuid().as_raw()
1372 || metadata.uid() != root_metadata.uid()
1373 || metadata.gid() != root_metadata.gid()
1374 {
1375 return Err(SqliteServingOwnerError::Invalid(
1376 "serving lock ownership, mode, or link count is invalid".to_string(),
1377 ));
1378 }
1379 }
1380 Ok(())
1381}
1382
1383#[cfg(unix)]
1384fn metadata_device(metadata: &fs::Metadata) -> Result<i64, SqliteServingOwnerError> {
1385 use std::os::unix::fs::MetadataExt;
1386 sqlite_u64(metadata.dev(), "device")
1387}
1388
1389#[cfg(not(unix))]
1390fn metadata_device(_metadata: &fs::Metadata) -> Result<i64, SqliteServingOwnerError> {
1391 Err(SqliteServingOwnerError::Invalid(
1392 "sqlite serving ownership requires Unix file identity".to_string(),
1393 ))
1394}
1395
1396#[cfg(unix)]
1397fn metadata_inode(metadata: &fs::Metadata) -> Result<i64, SqliteServingOwnerError> {
1398 use std::os::unix::fs::MetadataExt;
1399 sqlite_u64(metadata.ino(), "inode")
1400}
1401
1402#[cfg(not(unix))]
1403fn metadata_inode(_metadata: &fs::Metadata) -> Result<i64, SqliteServingOwnerError> {
1404 Err(SqliteServingOwnerError::Invalid(
1405 "sqlite serving ownership requires Unix file identity".to_string(),
1406 ))
1407}
1408
1409fn sqlite_u64(value: u64, field: &str) -> Result<i64, SqliteServingOwnerError> {
1410 i64::try_from(value).map_err(|_| {
1411 SqliteServingOwnerError::Invalid(format!("{field} exceeds SQLite INTEGER range"))
1412 })
1413}
1414
1415pub(super) fn read_u64(value: i64, field: &str) -> Result<u64, SqliteServingOwnerError> {
1416 u64::try_from(value)
1417 .map_err(|_| SqliteServingOwnerError::Invalid(format!("{field} is negative")))
1418}
1419
1420fn path_text(path: &Path) -> Result<String, SqliteServingOwnerError> {
1421 path.to_str()
1422 .map(ToOwned::to_owned)
1423 .ok_or_else(|| SqliteServingOwnerError::Invalid("path is not valid UTF-8".to_string()))
1424}
1425
1426fn now_ms() -> Result<i64, SqliteServingOwnerError> {
1427 let millis = SystemTime::now()
1428 .duration_since(UNIX_EPOCH)
1429 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?
1430 .as_millis();
1431 i64::try_from(millis)
1432 .map_err(|_| SqliteServingOwnerError::Invalid("wall clock overflowed i64".to_string()))
1433}
1434
1435fn authority_data_version(connection: &Connection) -> Result<u64, SqliteServingOwnerError> {
1436 let version = connection.query_row("PRAGMA data_version", [], |row| row.get::<_, i64>(0))?;
1437 read_u64(version, "sqlite data_version")
1438}
1439
1440fn verify_authority_store_invariants(
1441 connection: &Connection,
1442) -> Result<(), SqliteServingOwnerError> {
1443 verify_serving_owner_schema(connection)?;
1444 let foreign_key_violation = connection
1445 .query_row("PRAGMA foreign_key_check", [], |row| {
1446 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
1447 })
1448 .optional()?;
1449 if let Some((table, rowid)) = foreign_key_violation {
1450 return Err(SqliteServingOwnerError::Invalid(format!(
1451 "sqlite foreign key violation in `{table}` row {rowid}"
1452 )));
1453 }
1454 verify_serving_lease_history(connection)?;
1455 crate::budget_store::composite_schema::verify_budget_projection_invariants(connection)
1456 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
1457 verify_admission_authority_invariants(connection)
1458 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
1459 crate::admission_operation_store::verify_admission_operation_invariants(connection)
1460 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
1461 crate::tool_outcome_store::verify_tool_outcome_invariants(connection)
1462 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
1463 crate::frost_store::verify_frost_store_invariants(connection)
1464 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
1465 crate::economic_state_cache::verify_cache_sql_invariants(connection)
1466 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
1467 crate::fiscal_store::verify_fiscal_sql_invariants(connection)
1468 .map_err(|error| SqliteServingOwnerError::Invalid(error.to_string()))?;
1469 crate::channel_lifecycle_store::verify_channel_lifecycle_invariants(connection)?;
1470 crate::channel_release_publisher_store::verify_channel_release_publisher_invariants(
1471 connection,
1472 )?;
1473 Ok(())
1474}
1475
1476#[cfg(all(test, unix))]
1477#[path = "serving_owner/tests.rs"]
1478#[allow(clippy::expect_used, clippy::unwrap_used)]
1479mod tests;
1480
1481#[cfg(all(test, windows))]
1482mod windows_platform_tests {
1483 use super::*;
1484
1485 #[test]
1486 fn provision_rejects_windows_before_creating_database_ancestry(
1487 ) -> Result<(), Box<dyn std::error::Error>> {
1488 let temp = tempfile::tempdir()?;
1489 let lock_root = temp.path().join("locks");
1490 fs::create_dir(&lock_root)?;
1491 let database_parent = temp.path().join("state");
1492 let database = database_parent.join("authority.sqlite3");
1493
1494 let error = match SqliteAuthorityStore::provision(&database, &lock_root) {
1495 Ok(()) => {
1496 return Err(std::io::Error::other(
1497 "Windows authority provisioning unexpectedly succeeded",
1498 )
1499 .into());
1500 }
1501 Err(error) => error,
1502 };
1503
1504 assert!(matches!(
1505 error,
1506 SqliteServingOwnerError::Invalid(message)
1507 if message == UNSUPPORTED_SERVING_PLATFORM_MESSAGE
1508 ));
1509 assert!(!database_parent.exists());
1510 assert!(!database.exists());
1511 assert!(fs::read_dir(&lock_root)?.next().is_none());
1512 Ok(())
1513 }
1514
1515 #[test]
1516 fn open_serving_rejects_windows_before_accessing_paths(
1517 ) -> Result<(), Box<dyn std::error::Error>> {
1518 let temp = tempfile::tempdir()?;
1519 let database = temp.path().join("missing").join("authority.sqlite3");
1520 let lock_root = temp.path().join("missing-locks");
1521
1522 let error = match SqliteAuthorityStore::open_serving(&database, &lock_root) {
1523 Ok(_) => {
1524 return Err(std::io::Error::other(
1525 "Windows authority serving unexpectedly succeeded",
1526 )
1527 .into());
1528 }
1529 Err(error) => error,
1530 };
1531
1532 assert!(matches!(
1533 error,
1534 SqliteServingOwnerError::Invalid(message)
1535 if message == UNSUPPORTED_SERVING_PLATFORM_MESSAGE
1536 ));
1537 assert!(!database.exists());
1538 assert!(!lock_root.exists());
1539 Ok(())
1540 }
1541}