auths-core 0.1.3

Core cryptography and keychain integration for Auths
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! SQLite-based witness storage for persistence.
//!
//! This module provides persistent storage for witness state, enabling
//! the witness to recover its first-seen records and receipts after restart.
//!
//! # Schema
//!
//! Two tables are used:
//! - `first_seen`: Records (prefix, seq) → SAID mappings
//! - `receipts`: Stores issued receipts by (prefix, event_said)
//!
//! # Feature Gate
//!
//! This module requires the `witness-server` feature.

use std::path::Path;

use auths_keri::{Prefix, Said};
use chrono::{DateTime, Utc};
use sqlite::Connection;

use super::error::WitnessError;
use super::receipt::Receipt;
#[cfg(test)]
use super::receipt::ReceiptTag;

/// SQLite-based storage for witness state.
///
/// Stores first-seen records and issued receipts with WAL mode for
/// durability and concurrent reads.
pub struct WitnessStorage {
    conn: Connection,
}

impl WitnessStorage {
    /// Open or create a witness storage database.
    ///
    /// Creates the necessary tables if they don't exist.
    pub fn open(path: &Path) -> Result<Self, WitnessError> {
        let conn = Connection::open(path)
            .map_err(|e| WitnessError::Storage(format!("failed to open database: {}", e)))?;

        // Enable WAL mode for better concurrency
        conn.execute("PRAGMA journal_mode=WAL")
            .map_err(|e| WitnessError::Storage(format!("failed to enable WAL: {}", e)))?;

        let storage = Self { conn };
        storage.init_schema()?;
        Ok(storage)
    }

    /// Create an in-memory storage (for testing).
    pub fn in_memory() -> Result<Self, WitnessError> {
        let conn = Connection::open(":memory:")
            .map_err(|e| WitnessError::Storage(format!("failed to open in-memory db: {}", e)))?;

        let storage = Self { conn };
        storage.init_schema()?;
        Ok(storage)
    }

    /// Initialize the database schema.
    fn init_schema(&self) -> Result<(), WitnessError> {
        self.conn
            .execute(
                r#"
                CREATE TABLE IF NOT EXISTS first_seen (
                    prefix TEXT NOT NULL,
                    seq INTEGER NOT NULL,
                    said TEXT NOT NULL,
                    created_at TEXT NOT NULL,
                    PRIMARY KEY (prefix, seq)
                );

                CREATE TABLE IF NOT EXISTS receipts (
                    prefix TEXT NOT NULL,
                    event_said TEXT NOT NULL,
                    receipt_json TEXT NOT NULL,
                    created_at TEXT NOT NULL,
                    PRIMARY KEY (prefix, event_said)
                );

                CREATE INDEX IF NOT EXISTS idx_receipts_prefix ON receipts(prefix);
                "#,
            )
            .map_err(|e| WitnessError::Storage(format!("failed to create schema: {}", e)))?;

        Ok(())
    }

    /// Record a first-seen event.
    ///
    /// If the (prefix, seq) already exists, this is a no-op (idempotent).
    pub fn record_first_seen(
        &self,
        now: DateTime<Utc>,
        prefix: &Prefix,
        seq: u128,
        said: &Said,
    ) -> Result<(), WitnessError> {
        let now = now.to_rfc3339();

        let mut stmt = self.conn.prepare(
            "INSERT OR IGNORE INTO first_seen (prefix, seq, said, created_at) VALUES (?, ?, ?, ?)"
        ).map_err(|e| WitnessError::Storage(format!("failed to prepare record_first_seen: {}", e)))?;

        stmt.bind((1, prefix.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind prefix: {}", e)))?;
        stmt.bind((2, seq as i64))
            .map_err(|e| WitnessError::Storage(format!("failed to bind seq: {}", e)))?;
        stmt.bind((3, said.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind said: {}", e)))?;
        stmt.bind((4, now.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind now: {}", e)))?;

        stmt.next().map_err(|e| {
            WitnessError::Storage(format!("failed to execute record_first_seen: {}", e))
        })?;

        Ok(())
    }

    /// Get the first-seen SAID for a (prefix, seq).
    pub fn get_first_seen(&self, prefix: &Prefix, seq: u128) -> Result<Option<Said>, WitnessError> {
        let mut stmt = self
            .conn
            .prepare("SELECT said FROM first_seen WHERE prefix = ? AND seq = ?")
            .map_err(|e| {
                WitnessError::Storage(format!("failed to prepare get_first_seen: {}", e))
            })?;

        stmt.bind((1, prefix.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind prefix: {}", e)))?;
        stmt.bind((2, seq as i64))
            .map_err(|e| WitnessError::Storage(format!("failed to bind seq: {}", e)))?;

        if let Ok(sqlite::State::Row) = stmt.next() {
            let said: String = stmt
                .read(0)
                .map_err(|e| WitnessError::Storage(format!("failed to read said: {}", e)))?;
            Ok(Some(Said::new_unchecked(said)))
        } else {
            Ok(None)
        }
    }

    /// Check for duplicity: same (prefix, seq) with different SAID.
    ///
    /// Returns:
    /// - `Ok(None)` if no previous record or same SAID
    /// - `Ok(Some(existing_said))` if different SAID (duplicity!)
    pub fn check_duplicity(
        &self,
        now: DateTime<Utc>,
        prefix: &Prefix,
        seq: u128,
        said: &Said,
    ) -> Result<Option<Said>, WitnessError> {
        match self.get_first_seen(prefix, seq)? {
            None => {
                // First time seeing this (prefix, seq)
                self.record_first_seen(now, prefix, seq, said)?;
                Ok(None)
            }
            Some(existing) if existing == *said => {
                // Same SAID, no duplicity
                Ok(None)
            }
            Some(existing) => {
                // Different SAID - duplicity!
                Ok(Some(existing))
            }
        }
    }

    /// Store an issued receipt.
    pub fn store_receipt(
        &self,
        now: DateTime<Utc>,
        prefix: &Prefix,
        receipt: &Receipt,
    ) -> Result<(), WitnessError> {
        let now = now.to_rfc3339();
        let json = serde_json::to_string(receipt)
            .map_err(|e| WitnessError::Serialization(e.to_string()))?;

        let mut stmt = self.conn.prepare(
            "INSERT OR REPLACE INTO receipts (prefix, event_said, receipt_json, created_at) VALUES (?, ?, ?, ?)"
        ).map_err(|e| WitnessError::Storage(format!("failed to prepare store_receipt: {}", e)))?;

        stmt.bind((1, prefix.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind prefix: {}", e)))?;
        stmt.bind((2, receipt.d.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind event_said: {}", e)))?;
        stmt.bind((3, json.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind json: {}", e)))?;
        stmt.bind((4, now.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind now: {}", e)))?;

        stmt.next().map_err(|e| {
            WitnessError::Storage(format!("failed to execute store_receipt: {}", e))
        })?;

        Ok(())
    }

    /// Retrieve a receipt by prefix and event SAID.
    pub fn get_receipt(
        &self,
        prefix: &Prefix,
        event_said: &Said,
    ) -> Result<Option<Receipt>, WitnessError> {
        let mut stmt = self
            .conn
            .prepare("SELECT receipt_json FROM receipts WHERE prefix = ? AND event_said = ?")
            .map_err(|e| WitnessError::Storage(format!("failed to prepare get_receipt: {}", e)))?;

        stmt.bind((1, prefix.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind prefix: {}", e)))?;
        stmt.bind((2, event_said.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind event_said: {}", e)))?;

        if let Ok(sqlite::State::Row) = stmt.next() {
            let json: String = stmt.read(0).map_err(|e| {
                WitnessError::Storage(format!("failed to read receipt_json: {}", e))
            })?;
            let receipt = serde_json::from_str(&json)
                .map_err(|e| WitnessError::Serialization(e.to_string()))?;
            Ok(Some(receipt))
        } else {
            Ok(None)
        }
    }

    /// Get the latest sequence number seen for a prefix.
    pub fn get_latest_seq(&self, prefix: &Prefix) -> Result<Option<u64>, WitnessError> {
        let mut stmt = self
            .conn
            .prepare("SELECT MAX(seq) FROM first_seen WHERE prefix = ?")
            .map_err(|e| {
                WitnessError::Storage(format!("failed to prepare get_latest_seq: {}", e))
            })?;

        stmt.bind((1, prefix.as_str()))
            .map_err(|e| WitnessError::Storage(format!("failed to bind prefix: {}", e)))?;

        if let Ok(sqlite::State::Row) = stmt.next() {
            let seq: Option<i64> = stmt
                .read(0)
                .map_err(|e| WitnessError::Storage(format!("failed to read max seq: {}", e)))?;
            Ok(seq.map(|s| s as u64))
        } else {
            Ok(None)
        }
    }

    /// Count total first-seen records.
    pub fn count_first_seen(&self) -> Result<usize, WitnessError> {
        let mut stmt = self
            .conn
            .prepare("SELECT COUNT(*) FROM first_seen")
            .map_err(|e| {
                WitnessError::Storage(format!("failed to prepare count_first_seen: {}", e))
            })?;

        if let Ok(sqlite::State::Row) = stmt.next() {
            let count: i64 = stmt
                .read(0)
                .map_err(|e| WitnessError::Storage(format!("failed to read count: {}", e)))?;
            Ok(count as usize)
        } else {
            Ok(0)
        }
    }

    /// Count total receipts.
    pub fn count_receipts(&self) -> Result<usize, WitnessError> {
        let mut stmt = self
            .conn
            .prepare("SELECT COUNT(*) FROM receipts")
            .map_err(|e| {
                WitnessError::Storage(format!("failed to prepare count_receipts: {}", e))
            })?;

        if let Ok(sqlite::State::Row) = stmt.next() {
            let count: i64 = stmt
                .read(0)
                .map_err(|e| WitnessError::Storage(format!("failed to read count: {}", e)))?;
            Ok(count as usize)
        } else {
            Ok(0)
        }
    }
}

#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
    use super::*;

    fn prefix(s: &str) -> Prefix {
        Prefix::new_unchecked(s.into())
    }

    fn said(s: &str) -> Said {
        Said::new_unchecked(s.into())
    }

    fn now() -> DateTime<Utc> {
        Utc::now()
    }

    fn sample_receipt(event_said: &str) -> Receipt {
        use auths_keri::{KeriSequence, VersionString};
        Receipt {
            v: VersionString::placeholder(),
            t: ReceiptTag,
            d: Said::new_unchecked(event_said.into()),
            i: Prefix::new_unchecked("did:key:witness".into()),
            s: KeriSequence::new(5),
        }
    }

    #[test]
    fn storage_in_memory() {
        let storage = WitnessStorage::in_memory().unwrap();
        assert_eq!(storage.count_first_seen().unwrap(), 0);
    }

    #[test]
    fn record_and_get_first_seen() {
        let storage = WitnessStorage::in_memory().unwrap();
        let p = prefix("EPrefix");

        storage
            .record_first_seen(now(), &p, 0, &said("ESAID_A"))
            .unwrap();

        let result = storage.get_first_seen(&p, 0).unwrap();
        assert_eq!(result, Some(said("ESAID_A")));

        // Non-existent
        let result = storage.get_first_seen(&p, 1).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn first_seen_idempotent() {
        let storage = WitnessStorage::in_memory().unwrap();
        let p = prefix("EPrefix");

        storage
            .record_first_seen(now(), &p, 0, &said("ESAID_A"))
            .unwrap();
        storage
            .record_first_seen(now(), &p, 0, &said("ESAID_A"))
            .unwrap();

        assert_eq!(storage.count_first_seen().unwrap(), 1);
    }

    #[test]
    fn check_duplicity_first_time() {
        let storage = WitnessStorage::in_memory().unwrap();
        let p = prefix("EPrefix");

        let result = storage
            .check_duplicity(now(), &p, 0, &said("ESAID_A"))
            .unwrap();
        assert!(result.is_none());

        // Verify it was recorded
        assert_eq!(
            storage.get_first_seen(&p, 0).unwrap(),
            Some(said("ESAID_A"))
        );
    }

    #[test]
    fn check_duplicity_same_said() {
        let storage = WitnessStorage::in_memory().unwrap();
        let p = prefix("EPrefix");

        storage
            .record_first_seen(now(), &p, 0, &said("ESAID_A"))
            .unwrap();

        let result = storage
            .check_duplicity(now(), &p, 0, &said("ESAID_A"))
            .unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn check_duplicity_different_said() {
        let storage = WitnessStorage::in_memory().unwrap();
        let p = prefix("EPrefix");

        storage
            .record_first_seen(now(), &p, 0, &said("ESAID_A"))
            .unwrap();

        let result = storage
            .check_duplicity(now(), &p, 0, &said("ESAID_B"))
            .unwrap();
        assert_eq!(result, Some(said("ESAID_A")));
    }

    #[test]
    fn store_and_get_receipt() {
        let storage = WitnessStorage::in_memory().unwrap();
        let p = prefix("EPrefix");

        let receipt = sample_receipt("EEVENT_SAID");
        storage.store_receipt(now(), &p, &receipt).unwrap();

        let result = storage.get_receipt(&p, &said("EEVENT_SAID")).unwrap();
        assert!(result.is_some());
        let retrieved = result.unwrap();
        assert_eq!(retrieved.d.as_str(), "EEVENT_SAID");
    }

    #[test]
    fn get_receipt_not_found() {
        let storage = WitnessStorage::in_memory().unwrap();
        let p = prefix("EPrefix");

        let result = storage.get_receipt(&p, &said("ENONEXISTENT")).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn get_latest_seq() {
        let storage = WitnessStorage::in_memory().unwrap();
        let p = prefix("EPrefix");

        // No records
        assert!(storage.get_latest_seq(&p).unwrap().is_none());

        storage
            .record_first_seen(now(), &p, 0, &said("ES0"))
            .unwrap();
        storage
            .record_first_seen(now(), &p, 5, &said("ES5"))
            .unwrap();
        storage
            .record_first_seen(now(), &p, 3, &said("ES3"))
            .unwrap();

        assert_eq!(storage.get_latest_seq(&p).unwrap(), Some(5));
    }
}