Skip to main content

chio_store_sqlite/
lib.rs

1//! SQLite-backed persistence, query, and report layer for the Chio protocol.
2//!
3//! This crate is the concrete persistent backend for the kernel's receipt log
4//! and its supporting state. It implements the receipt store and query path,
5//! budget and approval stores, capability-lineage and revocation stores, an
6//! execution-nonce store, an encrypted-blob store, IOU and dead-letter stores,
7//! and evidence-export queries. The store traits it implements are defined by
8//! `chio-kernel` and `chio-core`. Reader-heavy receipt queries use a
9//! connection pool (eight readers by default); writes are serialized through a
10//! group-commit actor onto a single writer connection.
11//!
12//! # Modules
13//!
14//! - [`receipt_store`] / [`receipt_query`] -- receipt persistence and the
15//!   query path.
16//! - [`budget_store`] -- durable budget state.
17//! - [`approval_store`] / [`batch_approval_store`] -- human-approval state.
18//! - [`capability_lineage`] / [`revocation_store`] -- capability provenance and
19//!   revocation.
20//! - [`execution_nonce_store`] / [`dead_letters`] / [`iou_store`] -- nonce
21//!   replay guard, settlement dead letters, and IOU envelopes.
22//! - [`encrypted_blob`] / [`memory_provenance_store`] / [`evidence_export`] --
23//!   encrypted payloads, memory provenance, and evidence export.
24
25#![forbid(unsafe_code)]
26
27use std::path::{Path, PathBuf};
28
29pub mod admission_operation_store;
30pub mod approval_store;
31pub mod authority;
32pub mod batch_approval_store;
33pub mod budget_store;
34pub mod capability_lineage;
35pub mod channel_lifecycle_store;
36pub mod channel_release_publisher_store;
37pub mod clearing_lifecycle_store;
38pub mod dead_letters;
39pub mod economic_state_cache;
40pub mod encrypted_blob;
41pub mod evidence_export;
42pub mod execution_nonce_store;
43pub mod fiscal_store;
44pub mod frost_store;
45pub mod iou_store;
46#[cfg(feature = "lineage")]
47pub mod lineage_cte;
48pub mod memory_provenance_store;
49pub mod receipt_query;
50pub mod receipt_store;
51pub mod revocation_store;
52pub mod schema_version;
53pub mod serving_owner;
54pub mod settle_attempts;
55pub mod tool_outcome_store;
56
57pub use chio_core::crypto::SharedCanonicalBytes;
58pub use chio_core::{CanonicalBytes, CanonicalJsonWitness};
59pub use chio_kernel::{EvidenceChildReceiptScope, EvidenceExportQuery};
60
61/// Default SQLite reader pool size.
62///
63/// Reader-heavy receipt queries keep the existing eight-connection default.
64pub const DEFAULT_READER_POOL_MAX_SIZE: u32 = 8;
65
66/// Default SQLite writer pool size.
67///
68/// Receipt writes are serialized through the group-commit actor, so the
69/// writer pool defaults to a single connection.
70pub const DEFAULT_WRITER_POOL_MAX_SIZE: u32 = 1;
71
72/// SQLite pool sizing and per-connection growth bound for receipt-store read
73/// and write paths.
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct SqlitePoolConfig {
76    pub reader_pool_max_size: u32,
77    pub writer_pool_max_size: u32,
78    /// Optional `PRAGMA max_page_count` ceiling applied to every pooled
79    /// connection. An operational bound on the logical page count of the MAIN
80    /// database file: a write that would push the main file past the cap fails
81    /// closed with a full-database error. This bounds the main file only, not the
82    /// `-wal` sidecar, so it is not a whole-volume guard: under checkpoint
83    /// starvation the WAL can still grow unbounded. `None` (the default) leaves
84    /// SQLite's built-in page ceiling in place, so a store opened without this
85    /// knob behaves exactly as before.
86    pub max_page_count: Option<u32>,
87}
88
89impl Default for SqlitePoolConfig {
90    fn default() -> Self {
91        Self {
92            reader_pool_max_size: DEFAULT_READER_POOL_MAX_SIZE,
93            writer_pool_max_size: DEFAULT_WRITER_POOL_MAX_SIZE,
94            max_page_count: None,
95        }
96    }
97}
98
99/// Receipt-store construction options.
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub struct SqliteStoreOptions {
102    pub pool: SqlitePoolConfig,
103    /// When true (default), the append path uses the actor-owned verified
104    /// head (O(1) predecessor check + O(b) delta cross-check). When false,
105    /// the store keeps today's full per-append verification so operators can
106    /// A/B a suspect database. Read-only after open.
107    pub incremental_verification: bool,
108}
109
110impl Default for SqliteStoreOptions {
111    fn default() -> Self {
112        Self {
113            pool: SqlitePoolConfig::default(),
114            incremental_verification: true,
115        }
116    }
117}
118
119/// Whether a SQLite path opens a database that lives only in memory for the life
120/// of the process. rusqlite enables URI filenames, so the bare `:memory:`
121/// sentinel, `file::memory:`, and any `file:...?mode=memory` URI all open a
122/// non-durable database that loses its contents on restart and must not be
123/// mistaken for a durable store. Durability gates use this to refuse an in-memory
124/// path where they would otherwise advertise durable persistence.
125#[must_use]
126pub fn is_in_memory_sqlite_path(path: &str) -> bool {
127    if path.eq_ignore_ascii_case(":memory:") {
128        return true;
129    }
130    let Some(rest) = path.strip_prefix("file:") else {
131        return false;
132    };
133    let (name, query) = match rest.split_once('?') {
134        Some((name, query)) => (name, Some(query)),
135        None => (rest, None),
136    };
137    if name.eq_ignore_ascii_case(":memory:") {
138        return true;
139    }
140    query.is_some_and(|query| {
141        query
142            .split('&')
143            .any(|param| param.eq_ignore_ascii_case("mode=memory"))
144    })
145}
146
147/// The directory that must exist before SQLite opens `path`, or `None` when
148/// there is nothing to create.
149///
150/// rusqlite accepts `file:` URIs whose query string (`?mode=rwc`) and optional
151/// `//authority` are not part of the on-disk filename. Treating such a URI as a
152/// plain [`Path`] makes `parent()` resolve to a bogus directory (for example
153/// `file:/var/lib/chio`) and skips creating the real one, so SQLite then fails
154/// to open the database. This strips the `file:` scheme, any authority, and the
155/// query so callers create the directory that actually backs the database. An
156/// in-memory database (`:memory:`, `file:...?mode=memory`) has no backing
157/// directory and returns `None`.
158#[must_use]
159pub(crate) fn sqlite_parent_dir_to_create(path: &Path) -> Option<PathBuf> {
160    let Some(text) = path.to_str() else {
161        // A non-UTF8 path cannot be a `file:` URI, so use it verbatim.
162        return non_empty_parent(path);
163    };
164    if is_in_memory_sqlite_path(text) {
165        return None;
166    }
167    non_empty_parent(&sqlite_filesystem_path(text))
168}
169
170/// The parent of `path`, unless it is empty (a bare filename with no directory
171/// component), in which case there is nothing to create.
172fn non_empty_parent(path: &Path) -> Option<PathBuf> {
173    path.parent()
174        .filter(|parent| !parent.as_os_str().is_empty())
175        .map(Path::to_path_buf)
176}
177
178/// The filesystem path a rusqlite path points at, resolving a `file:` URI to
179/// its on-disk filename by stripping the scheme, any `//authority`, and the
180/// `?query`. A plain path (no `file:` scheme) is returned unchanged.
181#[must_use]
182pub fn sqlite_filesystem_path(text: &str) -> PathBuf {
183    let Some(rest) = text.strip_prefix("file:") else {
184        return PathBuf::from(text);
185    };
186    // Drop the URI query (`?mode=rwc`, `?cache=shared`); it is not part of the
187    // filename.
188    let without_query = rest.split_once('?').map_or(rest, |(name, _query)| name);
189    // `file://authority/path` places the filesystem path after the authority;
190    // `file:/path` and `file:path` have no authority. Strip a leading `//` and
191    // the authority up to the next `/`.
192    let filesystem = match without_query.strip_prefix("//") {
193        Some(after_authority_marker) => match after_authority_marker.find('/') {
194            Some(path_start) => &after_authority_marker[path_start..],
195            None => "",
196        },
197        None => without_query,
198    };
199    PathBuf::from(filesystem)
200}
201
202pub use admission_operation_store::{
203    CreditExposureAccountSnapshot, DurableObligationV1, SqliteAdmissionOperationStore,
204};
205pub use approval_store::SqliteApprovalStore;
206pub use authority::SqliteCapabilityAuthority;
207pub use batch_approval_store::SqliteBatchApprovalStore;
208pub use budget_store::{BudgetStoreSnapshot, SqliteBudgetStore};
209pub use channel_lifecycle_store::{
210    ChannelLifecycleStoreError, ChannelPreparedAdmissionRecordV1, ChannelPreparedBeginResult,
211    ChannelReservationDispositionV1, ChannelReservationStageRecordV1, SqliteChannelLifecycleStore,
212};
213pub use channel_release_publisher_store::{
214    ChannelReleasePublicationRecordV1, ChannelReleasePublicationStatusV1,
215    ChannelReleasePublisherError, ChannelReleaseSubmissionOutcomeV1,
216    SqliteChannelReleasePublisherStore, VerifiedChannelReleasePublicationV1,
217};
218pub use clearing_lifecycle_store::{ClearingLifecycleStoreError, SqliteClearingLifecycleStore};
219pub use economic_state_cache::{
220    admission_terminal_projection_effect_result, EconomicOperationStageBinding,
221    EconomicOperationStageContext, EconomicStateCacheError, EconomicStateStageDescriptor,
222    EconomicStateStageRecord, EconomicStateStageStatus, SqliteEconomicStateCache,
223};
224pub use encrypted_blob::{
225    decrypt_blob, encrypt_blob, BlobHandle, BlobStoreError, DecryptError, EncryptError,
226    EncryptedBlob, SqliteEncryptedBlobStore, TenantId, TenantKey,
227};
228pub use execution_nonce_store::{SqliteExecutionNonceStore, SqliteExecutionNonceStoreError};
229pub use frost_store::{
230    FrostActiveRosterRecord, FrostCeremonyRecord, FrostCeremonyRound1Record,
231    FrostCeremonyRound2Record, FrostCeremonyState, FrostCoordinatorCancellation,
232    FrostCoordinatorCommitment, FrostCoordinatorLease, FrostCoordinatorSessionRecord,
233    FrostCoordinatorSessionRequest, FrostCoordinatorSessionState, FrostCoordinatorShare,
234    FrostCoordinatorSigningPackage, FrostCustodyKey, FrostRotationRecord, FrostRotationState,
235    FrostSignerCommitment, FrostSignerSessionRecord, FrostSignerSessionRequest,
236    FrostSignerSessionState, FrostSignerShare, FrostStoreError, SqliteFrostStore,
237    StagedFrostRotation, StoredFrostCeremonyCompletion,
238};
239pub use iou_store::{SqliteIouEnvelopeStore, IOU_ENVELOPE_MIGRATION};
240pub use memory_provenance_store::{SqliteMemoryProvenanceStore, SqliteMemoryProvenanceStoreError};
241pub use receipt_store::{BackgroundCheckpointSigner, SqliteReceiptStore};
242pub use revocation_store::SqliteRevocationStore;
243pub use schema_version::{
244    check_schema_version, stamp_schema_version, SchemaVersionError, CHIO_SQLITE_APPLICATION_ID,
245};
246pub use serving_owner::{
247    scope_fixed_authority_ids_for_current_thread, FixedAuthorityIdScope, SqliteAuthorityStore,
248    SqliteServingOwnerError,
249};
250
251impl chio_kernel::QualifiedAdmissionProjectionStore
252    for admission_operation_store::SqliteAdmissionOperationStore
253{
254    fn load_payment_journal(
255        &self,
256        operation_id: &str,
257        active_fence: &chio_kernel::admission_operation::StoreMutationFence,
258    ) -> Result<
259        Option<chio_kernel::payment::PaymentJournalRecord>,
260        chio_kernel::AdmissionPaymentJournalError,
261    > {
262        admission_operation_store::SqliteAdmissionOperationStore::load_payment_journal(
263            self,
264            operation_id,
265            active_fence,
266        )
267    }
268
269    fn advance_payment_journal(
270        &self,
271        advance: chio_kernel::AdmissionPaymentJournalAdvance<'_>,
272    ) -> Result<chio_kernel::payment::PaymentJournalRecord, chio_kernel::AdmissionPaymentJournalError>
273    {
274        admission_operation_store::SqliteAdmissionOperationStore::advance_payment_journal(
275            self, advance,
276        )
277    }
278
279    fn begin_payment_settlement(
280        &self,
281        begin: chio_kernel::AdmissionPaymentSettlementBegin<'_>,
282    ) -> Result<chio_kernel::AdmissionPaymentSettlement, chio_kernel::AdmissionPaymentJournalError>
283    {
284        admission_operation_store::SqliteAdmissionOperationStore::begin_payment_settlement(
285            self, begin,
286        )
287    }
288
289    fn authorize_budget_and_commit_admission(
290        &self,
291        operation: &chio_kernel::admission_operation::AdmissionOperationV1,
292        recovery_lease: &chio_kernel::admission_operation::AdmissionRecoveryLease,
293        request: chio_kernel::budget_store::BudgetAuthorizeHoldRequest,
294        payment_journal: Option<chio_kernel::payment::PaymentJournalRecord>,
295        credit_exposure: Option<chio_kernel::CreditExposureReservationRequest>,
296        active_fence: &chio_kernel::admission_operation::StoreMutationFence,
297        trusted_now_unix_ms: u64,
298    ) -> Result<
299        chio_kernel::AdmissionBudgetAuthorization,
300        chio_kernel::AdmissionBudgetAuthorizationError,
301    > {
302        admission_operation_store::SqliteAdmissionOperationStore::authorize_budget_and_commit_admission(
303            self,
304            operation,
305            recovery_lease,
306            request,
307            payment_journal,
308            credit_exposure,
309            active_fence,
310            trusted_now_unix_ms,
311        )
312        .map(|(decision, operation)| chio_kernel::AdmissionBudgetAuthorization {
313            decision,
314            operation,
315        })
316        .map_err(|error| match error {
317            chio_kernel::admission_operation::AdmissionCaptureError::Unavailable(detail) => {
318                chio_kernel::AdmissionBudgetAuthorizationError::Unavailable(detail)
319            }
320            chio_kernel::admission_operation::AdmissionCaptureError::Fenced => {
321                chio_kernel::AdmissionBudgetAuthorizationError::Fenced
322            }
323            chio_kernel::admission_operation::AdmissionCaptureError::OutcomeUnknown(detail) => {
324                chio_kernel::AdmissionBudgetAuthorizationError::OutcomeUnknown(detail)
325            }
326            chio_kernel::admission_operation::AdmissionCaptureError::Invariant(detail) => {
327                chio_kernel::AdmissionBudgetAuthorizationError::Invariant(detail)
328            }
329            chio_kernel::admission_operation::AdmissionCaptureError::Operation(error) => {
330                chio_kernel::AdmissionBudgetAuthorizationError::Operation(error)
331            }
332        })
333    }
334
335    fn capture_invocation_and_commit_dispatch(
336        &self,
337        operation: &chio_kernel::admission_operation::AdmissionOperationV1,
338        recovery_lease: &chio_kernel::admission_operation::AdmissionRecoveryLease,
339        request: chio_kernel::budget_store::BudgetCaptureInvocationRequest,
340        active_fence: &chio_kernel::admission_operation::StoreMutationFence,
341        trusted_now_unix_ms: u64,
342    ) -> Result<
343        chio_kernel::AdmissionBudgetCapture,
344        chio_kernel::admission_operation::AdmissionCaptureError,
345    > {
346        admission_operation_store::SqliteAdmissionOperationStore::capture_invocation_and_commit_dispatch(
347            self,
348            operation,
349            recovery_lease,
350            request,
351            active_fence,
352            trusted_now_unix_ms,
353        )
354        .map(|(decision, operation)| chio_kernel::AdmissionBudgetCapture {
355            decision,
356            operation,
357        })
358    }
359
360    fn reserve_threshold_approval_and_commit_admission(
361        &self,
362        command: &chio_kernel::admission_operation::AdmissionOperationCommand,
363        reservation: &chio_kernel::ThresholdApprovalReplayReservationV1,
364        trusted_now_unix_ms: u64,
365    ) -> Result<
366        chio_kernel::admission_operation::AdmissionCommandResult,
367        chio_kernel::admission_operation::AdmissionOperationStoreError,
368    > {
369        admission_operation_store::SqliteAdmissionOperationStore::reserve_threshold_approval_and_commit_admission(
370            self,
371            command,
372            reservation,
373            trusted_now_unix_ms,
374        )
375    }
376
377    fn list_admission_receipts_after(
378        &self,
379        after_receipt_id: Option<&str>,
380        limit: usize,
381    ) -> Result<Vec<chio_core::receipt::body::ChioReceipt>, chio_kernel::ReceiptStoreError> {
382        self.list_terminal_receipts_after(after_receipt_id, limit)
383    }
384}
385
386impl chio_credit::obligation::CreditAdmissionStore
387    for admission_operation_store::SqliteAdmissionOperationStore
388{
389    fn lookup_record_by_operation(
390        &self,
391        operation_id: &str,
392    ) -> Result<
393        Option<chio_credit::obligation::CreditExposureReservationRecordV1>,
394        chio_credit::obligation::CreditAdmissionError,
395    > {
396        self.load_credit_exposure_reservation(operation_id)
397            .map_err(|error| {
398                chio_credit::obligation::CreditAdmissionError::Store(error.to_string())
399            })
400    }
401}
402
403impl chio_kernel::receipt_store::AnchoredAdmissionProjectionStore
404    for admission_operation_store::SqliteAdmissionOperationStore
405{
406    fn stage_anchored_terminal_projection(
407        &self,
408        advance: &chio_core::economic_continuity::VerifiedEconomicStateBatchAdvance,
409        recovery_lease: &chio_kernel::admission_operation::AdmissionRecoveryLease,
410        envelope: &chio_kernel::admission_operation::SignedAdmissionTerminalProjectionV1,
411        active_fence: &chio_kernel::admission_operation::StoreMutationFence,
412        trusted_now_unix_ms: u64,
413    ) -> Result<(), chio_kernel::ReceiptStoreError> {
414        admission_operation_store::SqliteAdmissionOperationStore::stage_anchored_terminal_projection(
415            self,
416            advance,
417            recovery_lease,
418            envelope,
419            active_fence,
420            trusted_now_unix_ms,
421        )
422        .map_err(admission_operation_store::receipt_projection_error)
423    }
424
425    fn qualify_anchored_terminal_projection(
426        &self,
427        batch_id: &str,
428        active_fence: &chio_kernel::admission_operation::StoreMutationFence,
429        trusted_now_unix_ms: u64,
430    ) -> Result<(), chio_kernel::ReceiptStoreError> {
431        admission_operation_store::SqliteAdmissionOperationStore::qualify_anchored_terminal_projection(
432            self,
433            batch_id,
434            active_fence,
435            trusted_now_unix_ms,
436        )
437        .map_err(admission_operation_store::receipt_projection_error)
438    }
439
440    fn record_anchored_terminal_projection(
441        &self,
442        advance: &chio_core::economic_continuity::VerifiedEconomicStateBatchAdvance,
443        committed: &chio_core::economic_continuity::VerifiedEconomicStateView,
444        pins: &chio_core::economic_continuity::EconomicStateAnchorPins,
445        active_fence: &chio_kernel::admission_operation::StoreMutationFence,
446        trusted_now_unix_ms: u64,
447    ) -> Result<(), chio_kernel::ReceiptStoreError> {
448        admission_operation_store::SqliteAdmissionOperationStore::record_anchored_terminal_projection(
449            self,
450            advance,
451            committed,
452            pins,
453            active_fence,
454            trusted_now_unix_ms,
455        )
456        .map_err(admission_operation_store::receipt_projection_error)
457    }
458
459    fn commit_anchored_terminal_projection(
460        &self,
461        batch_id: &str,
462        active_fence: &chio_kernel::admission_operation::StoreMutationFence,
463        trusted_now_unix_ms: u64,
464    ) -> Result<chio_kernel::admission_operation::AdmissionTerminal, chio_kernel::ReceiptStoreError>
465    {
466        admission_operation_store::SqliteAdmissionOperationStore::commit_anchored_terminal_projection(
467            self,
468            batch_id,
469            active_fence,
470            trusted_now_unix_ms,
471        )
472        .map_err(admission_operation_store::receipt_projection_error)
473    }
474}
475pub use settle_attempts::{SqliteSettlementOutcomeStore, SETTLE_ATTEMPTS_MIGRATION};
476pub use tool_outcome_store::SqliteToolOutcomeStore;
477
478#[cfg(test)]
479mod tests {
480    use super::{is_in_memory_sqlite_path, sqlite_parent_dir_to_create};
481    use std::path::{Path, PathBuf};
482
483    #[test]
484    fn resolves_parent_dir_from_file_uris_and_plain_paths() {
485        // A `file:` URI with a query resolves to the real filesystem parent, not
486        // a `file:`-prefixed directory folded out of the raw string.
487        assert_eq!(
488            sqlite_parent_dir_to_create(Path::new(
489                "file:/var/lib/chio/receipts.db.revocations?mode=rwc"
490            )),
491            Some(PathBuf::from("/var/lib/chio"))
492        );
493        // A `file://` URI with an empty authority drops the `//`.
494        assert_eq!(
495            sqlite_parent_dir_to_create(Path::new("file:///var/lib/chio/db?cache=shared")),
496            Some(PathBuf::from("/var/lib/chio"))
497        );
498        // A plain filesystem path keeps its parent unchanged.
499        assert_eq!(
500            sqlite_parent_dir_to_create(Path::new("/var/lib/chio/receipts.db")),
501            Some(PathBuf::from("/var/lib/chio"))
502        );
503        // A bare filename has no directory component to create.
504        assert_eq!(sqlite_parent_dir_to_create(Path::new("receipts.db")), None);
505        assert_eq!(
506            sqlite_parent_dir_to_create(Path::new("file:receipts.db?mode=rwc")),
507            None
508        );
509        // In-memory databases have no backing directory.
510        assert_eq!(sqlite_parent_dir_to_create(Path::new(":memory:")), None);
511        assert_eq!(
512            sqlite_parent_dir_to_create(Path::new("file:receipts.db?mode=memory")),
513            None
514        );
515    }
516
517    #[test]
518    fn classifies_in_memory_sqlite_paths() {
519        for path in [
520            ":memory:",
521            ":MEMORY:",
522            "file::memory:",
523            "file:receipts.db?mode=memory",
524            "file:receipts.db?cache=shared&mode=memory",
525        ] {
526            assert!(
527                is_in_memory_sqlite_path(path),
528                "{path} must classify as in-memory"
529            );
530        }
531    }
532
533    #[test]
534    fn classifies_durable_sqlite_paths() {
535        for path in [
536            "receipts.db",
537            "/var/lib/chio/receipts.db",
538            "file:/var/lib/chio/receipts.db?mode=rwc",
539            "file:receipts.db",
540            "memory-notes.db",
541        ] {
542            assert!(
543                !is_in_memory_sqlite_path(path),
544                "{path} must classify as durable"
545            );
546        }
547    }
548}