Skip to main content

chio_store_sqlite/
memory_provenance_store.rs

1//! SQLite-backed `MemoryProvenanceStore`.
2//!
3//! Durable append-only hash-chain of memory-write provenance entries.
4//! Keeps the same contract as `InMemoryMemoryProvenanceStore` in
5//! `chio-kernel::memory_provenance`: `append` computes the chain linkage
6//! atomically, `verify_entry` checks both the stored `hash` and the
7//! `prev_hash` linkage, and `chain_digest` returns the tail hash.
8//!
9//! Schema:
10//!
11//! ```sql
12//! CREATE TABLE chio_memory_provenance (
13//!     seq           INTEGER PRIMARY KEY AUTOINCREMENT,
14//!     entry_id      TEXT NOT NULL UNIQUE,
15//!     store         TEXT NOT NULL,
16//!     entry_key     TEXT NOT NULL,
17//!     capability_id TEXT NOT NULL,
18//!     receipt_id    TEXT NOT NULL,
19//!     written_at    INTEGER NOT NULL,
20//!     prev_hash     TEXT NOT NULL,
21//!     hash          TEXT NOT NULL
22//! );
23//! CREATE INDEX idx_chio_memory_provenance_key
24//!     ON chio_memory_provenance(store, entry_key, seq);
25//! ```
26//!
27//! The monotonic `seq` column is the chain position; `verify_entry`
28//! looks up the preceding row by `seq` to confirm linkage.
29
30use std::fs;
31use std::path::Path;
32
33use chio_kernel::{
34    recompute_memory_provenance_entry_hash, MemoryProvenanceAppend, MemoryProvenanceEntry,
35    MemoryProvenanceError, MemoryProvenanceStore, ProvenanceVerification, UnverifiedReason,
36    MEMORY_PROVENANCE_GENESIS_PREV_HASH,
37};
38use r2d2::Pool;
39use r2d2_sqlite::SqliteConnectionManager;
40use rusqlite::{params, OptionalExtension};
41use uuid::Uuid;
42
43/// Opaque error type for the SQLite-backed memory provenance store.
44#[derive(Debug)]
45pub struct SqliteMemoryProvenanceStoreError(String);
46
47impl std::fmt::Display for SqliteMemoryProvenanceStoreError {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(f, "sqlite memory provenance store error: {}", self.0)
50    }
51}
52
53impl std::error::Error for SqliteMemoryProvenanceStoreError {}
54
55impl From<rusqlite::Error> for SqliteMemoryProvenanceStoreError {
56    fn from(error: rusqlite::Error) -> Self {
57        Self(error.to_string())
58    }
59}
60
61impl From<std::io::Error> for SqliteMemoryProvenanceStoreError {
62    fn from(error: std::io::Error) -> Self {
63        Self(error.to_string())
64    }
65}
66
67impl From<r2d2::Error> for SqliteMemoryProvenanceStoreError {
68    fn from(error: r2d2::Error) -> Self {
69        Self(error.to_string())
70    }
71}
72
73impl From<SqliteMemoryProvenanceStoreError> for MemoryProvenanceError {
74    fn from(error: SqliteMemoryProvenanceStoreError) -> Self {
75        MemoryProvenanceError::Backend(error.0)
76    }
77}
78
79/// SQLite-backed durable memory-provenance chain.
80pub struct SqliteMemoryProvenanceStore {
81    pool: Pool<SqliteConnectionManager>,
82}
83
84/// Memory-provenance-store schema revision. Bump on every schema-affecting change.
85const MEMORY_PROVENANCE_STORE_SUPPORTED_SCHEMA_VERSION: i32 = 0;
86/// Stable key under which this store records its schema revision in the shared
87/// keyed metadata table, distinct from any co-located store's key.
88const MEMORY_PROVENANCE_STORE_SCHEMA_KEY: &str = "memory_provenance";
89/// Tables shipped before schema stamping existed, used to adopt a pre-stamping
90/// memory-provenance database rather than reject it as foreign.
91const MEMORY_PROVENANCE_STORE_LEGACY_ANCHOR_TABLES: &[&str] = &["chio_memory_provenance"];
92
93impl SqliteMemoryProvenanceStore {
94    /// Open the store at the given path, creating the parent directory
95    /// if needed.
96    pub fn open(path: impl AsRef<Path>) -> Result<Self, SqliteMemoryProvenanceStoreError> {
97        let path = path.as_ref();
98        // Resolve any `file:` URI to its on-disk parent before creating it, so a
99        // URI-configured store creates the real backing directory rather than a
100        // bogus scheme-prefixed one.
101        if let Some(parent) = crate::sqlite_parent_dir_to_create(path) {
102            fs::create_dir_all(&parent)?;
103        }
104        let manager = SqliteConnectionManager::file(path);
105        let pool = Pool::builder().max_size(8).build(manager)?;
106        let store = Self { pool };
107        store.run_migrations()?;
108        Ok(store)
109    }
110
111    /// Open an in-memory store for tests.
112    pub fn open_in_memory() -> Result<Self, SqliteMemoryProvenanceStoreError> {
113        let manager = SqliteConnectionManager::memory();
114        let pool = Pool::builder().max_size(1).build(manager)?;
115        let store = Self { pool };
116        store.run_migrations()?;
117        Ok(store)
118    }
119
120    fn run_migrations(&self) -> Result<(), SqliteMemoryProvenanceStoreError> {
121        let conn = self
122            .pool
123            .get()
124            .map_err(|error| SqliteMemoryProvenanceStoreError(format!("pool acquire: {error}")))?;
125        crate::check_schema_version(
126            &conn,
127            MEMORY_PROVENANCE_STORE_SCHEMA_KEY,
128            MEMORY_PROVENANCE_STORE_SUPPORTED_SCHEMA_VERSION,
129            MEMORY_PROVENANCE_STORE_LEGACY_ANCHOR_TABLES,
130        )
131        .map_err(|error| SqliteMemoryProvenanceStoreError(error.to_string()))?;
132        conn.execute_batch(
133            r#"
134            PRAGMA journal_mode = WAL;
135            PRAGMA synchronous = FULL;
136            PRAGMA busy_timeout = 5000;
137
138            CREATE TABLE IF NOT EXISTS chio_memory_provenance (
139                seq           INTEGER PRIMARY KEY AUTOINCREMENT,
140                entry_id      TEXT NOT NULL UNIQUE,
141                store         TEXT NOT NULL,
142                entry_key     TEXT NOT NULL,
143                capability_id TEXT NOT NULL,
144                receipt_id    TEXT NOT NULL,
145                written_at    INTEGER NOT NULL,
146                prev_hash     TEXT NOT NULL,
147                hash          TEXT NOT NULL
148            );
149
150            CREATE INDEX IF NOT EXISTS idx_chio_memory_provenance_key
151                ON chio_memory_provenance(store, entry_key, seq);
152            "#,
153        )?;
154        crate::stamp_schema_version(
155            &conn,
156            MEMORY_PROVENANCE_STORE_SCHEMA_KEY,
157            MEMORY_PROVENANCE_STORE_SUPPORTED_SCHEMA_VERSION,
158        )
159        .map_err(|error| SqliteMemoryProvenanceStoreError(error.to_string()))?;
160        Ok(())
161    }
162
163    /// Test helper: overwrite an existing entry's `hash` column to
164    /// simulate tamper. Returns `false` when the row was not found.
165    pub fn tamper_entry_hash(
166        &self,
167        entry_id: &str,
168        forged_hash: &str,
169    ) -> Result<bool, SqliteMemoryProvenanceStoreError> {
170        let conn = self
171            .pool
172            .get()
173            .map_err(|error| SqliteMemoryProvenanceStoreError(format!("pool acquire: {error}")))?;
174        let updated = conn.execute(
175            "UPDATE chio_memory_provenance SET hash = ?1 WHERE entry_id = ?2",
176            params![forged_hash, entry_id],
177        )?;
178        Ok(updated > 0)
179    }
180}
181
182fn map_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<MemoryProvenanceEntry> {
183    Ok(MemoryProvenanceEntry {
184        entry_id: row.get("entry_id")?,
185        store: row.get("store")?,
186        key: row.get("entry_key")?,
187        capability_id: row.get("capability_id")?,
188        receipt_id: row.get("receipt_id")?,
189        written_at: row.get::<_, i64>("written_at")? as u64,
190        prev_hash: row.get("prev_hash")?,
191        hash: row.get("hash")?,
192    })
193}
194
195impl MemoryProvenanceStore for SqliteMemoryProvenanceStore {
196    fn append(
197        &self,
198        input: MemoryProvenanceAppend,
199    ) -> Result<MemoryProvenanceEntry, MemoryProvenanceError> {
200        let mut conn = self
201            .pool
202            .get()
203            .map_err(|error| MemoryProvenanceError::Backend(format!("pool acquire: {error}")))?;
204        // `IMMEDIATE` guarantees a write lock is taken before any
205        // subsequent read inside the transaction -- two concurrent
206        // appenders cannot both observe the same tail and fork the
207        // chain.
208        let tx = conn
209            .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
210            .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?;
211
212        let existing = tx
213            .query_row(
214                r#"
215                SELECT entry_id, store, entry_key, capability_id, receipt_id,
216                       written_at, prev_hash, hash
217                FROM chio_memory_provenance
218                WHERE receipt_id = ?1
219                LIMIT 1
220                "#,
221                params![input.receipt_id.as_str()],
222                map_row,
223            )
224            .optional()
225            .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?;
226        if let Some(existing) = existing {
227            if existing.store != input.store
228                || existing.key != input.key
229                || existing.capability_id != input.capability_id
230                || existing.written_at != input.written_at
231            {
232                return Err(MemoryProvenanceError::Backend(
233                    "memory provenance receipt id was reused with different fields".to_string(),
234                ));
235            }
236            tx.commit()
237                .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?;
238            return Ok(existing);
239        }
240
241        let prev_hash: String = tx
242            .query_row(
243                "SELECT hash FROM chio_memory_provenance ORDER BY seq DESC LIMIT 1",
244                [],
245                |row| row.get(0),
246            )
247            .optional()
248            .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?
249            .unwrap_or_else(|| MEMORY_PROVENANCE_GENESIS_PREV_HASH.to_string());
250
251        let entry_id = format!("mem-prov-{}", Uuid::now_v7());
252        let hash = recompute_memory_provenance_entry_hash(
253            &entry_id,
254            &input.store,
255            &input.key,
256            &input.capability_id,
257            &input.receipt_id,
258            input.written_at,
259            &prev_hash,
260        )?;
261
262        let written_at_i64 = i64::try_from(input.written_at).unwrap_or(i64::MAX);
263        tx.execute(
264            r#"
265            INSERT INTO chio_memory_provenance
266                (entry_id, store, entry_key, capability_id, receipt_id, written_at, prev_hash, hash)
267            VALUES
268                (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
269            "#,
270            params![
271                entry_id,
272                input.store,
273                input.key,
274                input.capability_id,
275                input.receipt_id,
276                written_at_i64,
277                prev_hash,
278                hash,
279            ],
280        )
281        .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?;
282
283        tx.commit()
284            .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?;
285
286        Ok(MemoryProvenanceEntry {
287            entry_id,
288            store: input.store,
289            key: input.key,
290            capability_id: input.capability_id,
291            receipt_id: input.receipt_id,
292            written_at: input.written_at,
293            prev_hash,
294            hash,
295        })
296    }
297
298    fn get_entry(
299        &self,
300        entry_id: &str,
301    ) -> Result<Option<MemoryProvenanceEntry>, MemoryProvenanceError> {
302        let conn = self
303            .pool
304            .get()
305            .map_err(|error| MemoryProvenanceError::Backend(format!("pool acquire: {error}")))?;
306        let row = conn
307            .query_row(
308                r#"
309                SELECT entry_id, store, entry_key, capability_id, receipt_id,
310                       written_at, prev_hash, hash
311                FROM chio_memory_provenance
312                WHERE entry_id = ?1
313                "#,
314                params![entry_id],
315                map_row,
316            )
317            .optional()
318            .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?;
319        Ok(row)
320    }
321
322    fn latest_for_key(
323        &self,
324        store: &str,
325        key: &str,
326    ) -> Result<Option<MemoryProvenanceEntry>, MemoryProvenanceError> {
327        let conn = self
328            .pool
329            .get()
330            .map_err(|error| MemoryProvenanceError::Backend(format!("pool acquire: {error}")))?;
331        let row = conn
332            .query_row(
333                r#"
334                SELECT entry_id, store, entry_key, capability_id, receipt_id,
335                       written_at, prev_hash, hash
336                FROM chio_memory_provenance
337                WHERE store = ?1 AND entry_key = ?2
338                ORDER BY seq DESC
339                LIMIT 1
340                "#,
341                params![store, key],
342                map_row,
343            )
344            .optional()
345            .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?;
346        Ok(row)
347    }
348
349    fn verify_entry(
350        &self,
351        entry_id: &str,
352    ) -> Result<ProvenanceVerification, MemoryProvenanceError> {
353        let conn = self
354            .pool
355            .get()
356            .map_err(|error| MemoryProvenanceError::Backend(format!("pool acquire: {error}")))?;
357
358        // Fetch the candidate row plus its seq and the prev row's hash
359        // in the same query so verification is a single round-trip.
360        let row = conn
361            .query_row(
362                r#"
363                SELECT seq, entry_id, store, entry_key, capability_id, receipt_id,
364                       written_at, prev_hash, hash
365                FROM chio_memory_provenance
366                WHERE entry_id = ?1
367                "#,
368                params![entry_id],
369                |row| {
370                    let seq: i64 = row.get("seq")?;
371                    let entry = map_row(row)?;
372                    Ok((seq, entry))
373                },
374            )
375            .optional()
376            .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?;
377
378        let Some((seq, entry)) = row else {
379            return Ok(ProvenanceVerification::Unverified {
380                reason: UnverifiedReason::NoProvenance,
381            });
382        };
383
384        let expected = entry.expected_hash()?;
385        if expected != entry.hash {
386            return Ok(ProvenanceVerification::Unverified {
387                reason: UnverifiedReason::ChainTampered,
388            });
389        }
390
391        let expected_prev: String = conn
392            .query_row(
393                r#"
394                SELECT hash
395                FROM chio_memory_provenance
396                WHERE seq < ?1
397                ORDER BY seq DESC
398                LIMIT 1
399                "#,
400                params![seq],
401                |row| row.get(0),
402            )
403            .optional()
404            .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?
405            .unwrap_or_else(|| MEMORY_PROVENANCE_GENESIS_PREV_HASH.to_string());
406
407        if expected_prev != entry.prev_hash {
408            return Ok(ProvenanceVerification::Unverified {
409                reason: UnverifiedReason::ChainLinkBroken,
410            });
411        }
412
413        let chain_digest: String = conn
414            .query_row(
415                "SELECT hash FROM chio_memory_provenance ORDER BY seq DESC LIMIT 1",
416                [],
417                |row| row.get(0),
418            )
419            .optional()
420            .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?
421            .unwrap_or_else(|| MEMORY_PROVENANCE_GENESIS_PREV_HASH.to_string());
422
423        Ok(ProvenanceVerification::Verified {
424            entry,
425            chain_digest,
426        })
427    }
428
429    fn chain_digest(&self) -> Result<String, MemoryProvenanceError> {
430        let conn = self
431            .pool
432            .get()
433            .map_err(|error| MemoryProvenanceError::Backend(format!("pool acquire: {error}")))?;
434        let digest: Option<String> = conn
435            .query_row(
436                "SELECT hash FROM chio_memory_provenance ORDER BY seq DESC LIMIT 1",
437                [],
438                |row| row.get(0),
439            )
440            .optional()
441            .map_err(|error| MemoryProvenanceError::Backend(error.to_string()))?;
442        Ok(digest.unwrap_or_else(|| MEMORY_PROVENANCE_GENESIS_PREV_HASH.to_string()))
443    }
444}