1use 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#[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
79pub struct SqliteMemoryProvenanceStore {
81 pool: Pool<SqliteConnectionManager>,
82}
83
84const MEMORY_PROVENANCE_STORE_SUPPORTED_SCHEMA_VERSION: i32 = 0;
86const MEMORY_PROVENANCE_STORE_SCHEMA_KEY: &str = "memory_provenance";
89const MEMORY_PROVENANCE_STORE_LEGACY_ANCHOR_TABLES: &[&str] = &["chio_memory_provenance"];
92
93impl SqliteMemoryProvenanceStore {
94 pub fn open(path: impl AsRef<Path>) -> Result<Self, SqliteMemoryProvenanceStoreError> {
97 let path = path.as_ref();
98 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 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 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 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 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}