Skip to main content

chio_store_sqlite/
serving_owner.rs

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    /// Rejects platforms that cannot provide the filesystem identity and
233    /// positioned I/O required by durable authority serving.
234    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        // Refuse a store the global baseline cannot adopt before anything is created,
371        // so a refused provision leaves the pre-existing database exactly as it was.
372        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        // The owner table and its row are created in a single transaction so a
377        // failure rolls the table back with the row. Nothing that depends on the
378        // owner table is created before that transaction commits, which leaves an
379        // interrupted provision recoverable rather than wedged half way.
380        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                // An unknown commit outcome stays untouched for manual inspection.
428                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    // Provisioning writes before `open_serving` sets its own pragmas, so the busy
1261    // timeout has to be in place here or a transient lock aborts it instantly.
1262    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}