| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | |
|
| 9 | | use crate::event::{canonical_json, Auth, Event, Method, NewEvent}; |
| 10 | | use crate::hlc::{Clock, Hlc}; |
| 11 | | use crate::identity::KeyId; |
| 12 | | use crate::identity::{Identity, Signature}; |
| 13 | | use crate::keys::KeyPair; |
| 14 | | use crate::{ids, projections}; |
| 15 | | use anyhow::{bail, Context, Result}; |
| 16 | | use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteRow, SqliteSynchronous}; |
| 17 | | use sqlx::{Row, SqlitePool}; |
| 18 | | use std::path::Path; |
| 19 | | use std::sync::Arc; |
| 20 | | use tokio::sync::{broadcast, Mutex}; |
| 21 | |
|
| 22 | |
|
| 23 | | pub const CHECKPOINT_EVERY: u64 = 256; |
| 24 | |
|
| 25 | | pub const CHECKPOINT_INTERVAL_MS: u64 = 10 * 60 * 1000; |
| 26 | |
|
| 27 | | const CHECKPOINT_DOMAIN: &[u8] = b"spectra1 checkpoint "; |
| 28 | |
|
| 29 | |
|
| 30 | | pub struct Ledger { |
| 31 | | pool: SqlitePool, |
| 32 | | |
| 33 | | |
| 34 | | _lock: Option<std::fs::File>, |
| 35 | | w: Arc<Writer>, |
| 36 | | } |
| 37 | |
|
| 38 | |
|
| 39 | |
|
| 40 | |
|
| 41 | |
|
| 42 | |
|
| 43 | | struct Writer { |
| 44 | | pool: SqlitePool, |
| 45 | | head: Mutex<Head>, |
| 46 | | clock: Clock, |
| 47 | | bus: broadcast::Sender<Arc<Event>>, |
| 48 | | instance: Option<KeyPair>, |
| 49 | | } |
| 50 | |
|
| 51 | | #[derive(Clone, Copy, Debug)] |
| 52 | | struct Head { |
| 53 | | seq: u64, |
| 54 | | hash: [u8; 32], |
| 55 | | since_checkpoint: u64, |
| 56 | | oldest_unchecked_ms: Option<u64>, |
| 57 | | } |
| 58 | |
|
| 59 | | impl Ledger { |
| 60 | | |
| 61 | | |
| 62 | | |
| 63 | 161× | pub async fn open(path: &Path, instance: Option<KeyPair>) -> Result<Ledger> { |
| 64 | 161× | let lock_path = path.with_extension("lock"); |
| 65 | 161× | let lock = std::fs::OpenOptions::new() |
| 66 | 161× | .create(true) |
| 67 | 161× | .truncate(false) |
| 68 | 161× | .write(true) |
| 69 | 161× | .open(&lock_path) |
| 70 | 161× | .with_context(|| format!("opening {}", lock_path.display()))?; |
| 71 | 161× | match lock.try_lock() { |
| 72 | 160× | Ok(()) => {} |
| 73 | | Err(std::fs::TryLockError::WouldBlock) => { |
| 74 | 1× | bail!("another process is writing to {} (is `spectra serve` running? stop it, or do this through it)", path.display()) |
| 75 | | } |
| 76 | 0× | Err(std::fs::TryLockError::Error(e)) => return Err(e).with_context(|| format!("locking {}", lock_path.display())), |
| 77 | | } |
| 78 | 160× | let mut l = Ledger::open_unlocked(path, instance).await?; |
| 79 | 159× | l._lock = Some(lock); |
| 80 | 159× | Ok(l) |
| 81 | 161× | } |
| 82 | |
|
| 83 | | |
| 84 | | |
| 85 | 79× | pub async fn open_read_only(path: &Path) -> Result<Ledger> { |
| 86 | 79× | Ledger::open_unlocked(path, None).await |
| 87 | 79× | } |
| 88 | |
|
| 89 | 239× | async fn open_unlocked(path: &Path, instance: Option<KeyPair>) -> Result<Ledger> { |
| 90 | 239× | let opts = SqliteConnectOptions::new() |
| 91 | 239× | .filename(path) |
| 92 | 239× | .create_if_missing(true) |
| 93 | 239× | .journal_mode(SqliteJournalMode::Wal) |
| 94 | 239× | .synchronous(SqliteSynchronous::Normal) |
| 95 | 239× | .foreign_keys(true) |
| 96 | 239× | .busy_timeout(std::time::Duration::from_secs(10)); |
| 97 | 239× | let pool = |
| 98 | 239× | SqlitePoolOptions::new().max_connections(8).connect_with(opts).await.with_context(|| format!("opening {}", path.display()))?; |
| 99 | 239× | sqlx::migrate!("./migrations").run(&pool).await.context("applying migrations")?; |
| 100 | 239× | let format: Option<String> = sqlx::query_scalar("SELECT value FROM meta WHERE key = 'ledger_format'").fetch_optional(&pool).await?; |
| 101 | 239× | match format.as_deref().map(str::parse::<u32>) { |
| 102 | | None => { |
| 103 | 54× | sqlx::query("INSERT INTO meta (key, value) VALUES ('ledger_format', ?)") |
| 104 | 54× | .bind(crate::LEDGER_FORMAT.to_string()) |
| 105 | 54× | .execute(&pool) |
| 106 | 54× | .await?; |
| 107 | | } |
| 108 | 185× | Some(Ok(f)) if f <= crate::LEDGER_FORMAT => {} |
| 109 | | Some(_) => { |
| 110 | 1× | bail!("{} was written by a newer Spectra (ledger format {}): upgrade spectra", path.display(), format.unwrap_or_default()) |
| 111 | | } |
| 112 | | } |
| 113 | 238× | let last = |
| 114 | 238× | sqlx::query("SELECT seq, hash, hlc_wall, hlc_logical FROM events ORDER BY seq DESC LIMIT 1").fetch_optional(&pool).await?; |
| 115 | 238× | let (seq, hash, hlc) = match last { |
| 116 | 184× | Some(r) => ( |
| 117 | 184× | r.get::<i64, _>(0) as u64, |
| 118 | 184× | to32(r.get::<Vec<u8>, _>(1))?, |
| 119 | 184× | Hlc { wall_ms: r.get::<i64, _>(2) as u64, logical: r.get::<i64, _>(3) as u32 }, |
| 120 | | ), |
| 121 | 54× | None => (0, [0; 32], Hlc::default()), |
| 122 | | }; |
| 123 | 238× | let checkpoint_seq: i64 = |
| 124 | 238× | sqlx::query_scalar("SELECT COALESCE(MAX(seq), 0) FROM events WHERE kind = 'ledger.checkpoint'").fetch_one(&pool).await?; |
| 125 | 238× | let since = seq.saturating_sub(checkpoint_seq as u64); |
| 126 | 238× | let (bus, _) = broadcast::channel(4096); |
| 127 | 238× | Ok(Ledger { |
| 128 | 238× | pool: pool.clone(), |
| 129 | 238× | _lock: None, |
| 130 | 238× | w: Arc::new(Writer { |
| 131 | 238× | pool, |
| 132 | 238× | head: Mutex::new(Head { seq, hash, since_checkpoint: since, oldest_unchecked_ms: (since > 0).then(ids::now_ms) }), |
| 133 | 238× | clock: Clock::starting_at(hlc), |
| 134 | 238× | bus, |
| 135 | 238× | instance, |
| 136 | 238× | }), |
| 137 | 238× | }) |
| 138 | 239× | } |
| 139 | |
|
| 140 | | |
| 141 | 843× | pub fn pool(&self) -> &SqlitePool { |
| 142 | 843× | &self.pool |
| 143 | 843× | } |
| 144 | |
|
| 145 | | |
| 146 | 32× | pub fn subscribe(&self) -> broadcast::Receiver<Arc<Event>> { |
| 147 | 32× | self.w.bus.subscribe() |
| 148 | 32× | } |
| 149 | |
|
| 150 | | |
| 151 | 0× | pub fn instance(&self) -> Option<&KeyPair> { |
| 152 | 0× | self.w.instance.as_ref() |
| 153 | 0× | } |
| 154 | |
|
| 155 | | |
| 156 | 100× | pub async fn head(&self) -> (u64, [u8; 32]) { |
| 157 | 100× | let h = self.w.head.lock().await; |
| 158 | 100× | (h.seq, h.hash) |
| 159 | 100× | } |
| 160 | |
|
| 161 | | |
| 162 | | |
| 163 | | |
| 164 | 2408× | pub async fn append(&self, events: Vec<NewEvent>) -> Result<Vec<Arc<Event>>> { |
| 165 | 2408× | let w = self.w.clone(); |
| 166 | 2408× | tokio::spawn(async move { w.append(events).await }).await.context("the ledger's writer stopped")? |
| 167 | 2108× | } |
| 168 | |
|
| 169 | | |
| 170 | 1859× | pub async fn append_one(&self, e: NewEvent) -> Result<Arc<Event>> { |
| 171 | 1859× | Ok(self.append(vec![e]).await?.remove(0)) |
| 172 | 1559× | } |
| 173 | |
|
| 174 | | |
| 175 | | |
| 176 | 140× | pub async fn checkpoint(&self) -> Result<Option<Arc<Event>>> { |
| 177 | 140× | let w = self.w.clone(); |
| 178 | 140× | tokio::spawn(async move { w.checkpoint().await }).await.context("the ledger's writer stopped")? |
| 179 | 140× | } |
| 180 | |
|
| 181 | | |
| 182 | 91× | pub async fn events(&self, from: u64, limit: u32) -> Result<Vec<Event>> { |
| 183 | 91× | let rows = sqlx::query(&format!("{SELECT_EVENT} WHERE seq >= ? ORDER BY seq LIMIT ?")) |
| 184 | 91× | .bind(from as i64) |
| 185 | 91× | .bind(limit as i64) |
| 186 | 91× | .fetch_all(&self.pool) |
| 187 | 91× | .await?; |
| 188 | 91× | rows.iter().map(row_to_event).collect() |
| 189 | 91× | } |
| 190 | |
|
| 191 | | |
| 192 | 511× | pub async fn events_of_kinds(&self, from: u64, kinds: &[&str], limit: u32) -> Result<Vec<Event>> { |
| 193 | 511× | let marks = vec!["?"; kinds.len()].join(","); |
| 194 | 511× | let sql = format!("{SELECT_EVENT} WHERE seq >= ? AND kind IN ({marks}) ORDER BY seq LIMIT ?"); |
| 195 | 511× | let mut q = sqlx::query(&sql).bind(from as i64); |
| 196 | 2691× | for k in kinds { |
| 197 | 2691× | q = q.bind(*k); |
| 198 | 2691× | } |
| 199 | 511× | let rows = q.bind(limit as i64).fetch_all(&self.pool).await?; |
| 200 | 507× | rows.iter().map(row_to_event).collect() |
| 201 | 507× | } |
| 202 | |
|
| 203 | | |
| 204 | | |
| 205 | 42× | pub async fn verify(&self, keys: &mut dyn KeyResolver) -> Result<Verification> { |
| 206 | 42× | let mut v = Verification::default(); |
| 207 | 42× | let mut prev = [0u8; 32]; |
| 208 | 42× | let mut expect = 1u64; |
| 209 | 42× | let mut instance: Option<Identity> = None; |
| 210 | 42× | let mut from = 1; |
| 211 | | loop { |
| 212 | 89× | let batch = self.events(from, 2_000).await?; |
| 213 | 89× | if batch.is_empty() { |
| 214 | 42× | break; |
| 215 | 47× | } |
| 216 | 11275× | for e in &batch { |
| 217 | 11275× | v.events += 1; |
| 218 | 11275× | if e.seq != expect { |
| 219 | 1× | v.finding(e.seq, Problem::Gap, format!("expected event {expect}")); |
| 220 | 1× | expect = e.seq; |
| 221 | 11274× | } |
| 222 | 11275× | if e.prev != prev { |
| 223 | 2× | v.finding(e.seq, Problem::ChainBreak, "prev is not the hash of the event before".into()); |
| 224 | 11273× | } |
| 225 | 11275× | if e.payload.is_some() && e.computed_hash() != e.hash { |
| 226 | 2× | v.finding(e.seq, Problem::HashMismatch, "the event's fields do not give its hash: it was edited".into()); |
| 227 | 11273× | } |
| 228 | 11275× | if e.kind == "identity.instance" { |
| 229 | 42× | instance = e |
| 230 | 42× | .json() |
| 231 | 42× | .get("identity") |
| 232 | 42× | .and_then(|h| h.as_str()) |
| 233 | 42× | .and_then(|h| hex::decode(h).ok()) |
| 234 | 42× | .and_then(|b| Identity::decode(&b).ok()); |
| 235 | 11233× | } |
| 236 | 11275× | if e.kind == "ledger.checkpoint" { |
| 237 | 140× | v.checkpoints += 1; |
| 238 | 140× | match check_checkpoint(e, prev, instance.as_ref()) { |
| 239 | 138× | Ok(()) => v.last_checkpoint = Some(e.seq), |
| 240 | 2× | Err(why) => v.finding(e.seq, Problem::BadCheckpoint, why), |
| 241 | | } |
| 242 | 11135× | } else if let Err(why) = keys.check(e) { |
| 243 | 10× | v.finding(e.seq, Problem::BadProof, why); |
| 244 | 11125× | } |
| 245 | 11275× | prev = e.hash; |
| 246 | 11275× | expect += 1; |
| 247 | | } |
| 248 | 47× | from = batch.last().map(|e| e.seq + 1).unwrap_or(from); |
| 249 | | } |
| 250 | 42× | v.head = prev; |
| 251 | 42× | v.uncovered = v.events.saturating_sub(v.last_checkpoint.unwrap_or(0)); |
| 252 | 42× | Ok(v) |
| 253 | 42× | } |
| 254 | |
|
| 255 | | |
| 256 | 5× | pub async fn rebuild(&self) -> Result<()> { |
| 257 | 5× | let w = self.w.clone(); |
| 258 | 5× | tokio::spawn(async move { w.rebuild().await }).await.context("the ledger's writer stopped")? |
| 259 | 5× | } |
| 260 | |
|
| 261 | | |
| 262 | | |
| 263 | 13× | pub async fn check(&self) -> Result<Vec<String>> { |
| 264 | 13× | let w = self.w.clone(); |
| 265 | 13× | tokio::spawn(async move { w.check().await }).await.context("the ledger's writer stopped")? |
| 266 | 13× | } |
| 267 | | } |
| 268 | |
|
| 269 | | impl Writer { |
| 270 | | |
| 271 | | |
| 272 | 2408× | async fn append(&self, events: Vec<NewEvent>) -> Result<Vec<Arc<Event>>> { |
| 273 | 2408× | let mut head = self.head.lock().await; |
| 274 | 2408× | let mut tx = self.pool.begin().await?; |
| 275 | 2408× | let mut out = Vec::with_capacity(events.len() + 1); |
| 276 | 2408× | let mut h = *head; |
| 277 | 12218× | for n in events { |
| 278 | 12218× | let e = self.chain(&mut h, n); |
| 279 | 12218× | insert(&mut tx, &e).await?; |
| 280 | 12218× | projections::apply(&mut tx, &e).await?; |
| 281 | 12218× | h.since_checkpoint += 1; |
| 282 | 12218× | h.oldest_unchecked_ms.get_or_insert(e.at_ms); |
| 283 | 12218× | out.push(e); |
| 284 | | |
| 285 | 12218× | if h.since_checkpoint >= CHECKPOINT_EVERY && self.instance.is_some() { |
| 286 | 44× | let cp = self.checkpoint_event(&mut h); |
| 287 | 44× | insert(&mut tx, &cp).await?; |
| 288 | 44× | projections::apply(&mut tx, &cp).await?; |
| 289 | 44× | out.push(cp); |
| 290 | 12174× | } |
| 291 | | } |
| 292 | 2408× | let due = h.oldest_unchecked_ms.is_some_and(|t| ids::now_ms().saturating_sub(t) >= CHECKPOINT_INTERVAL_MS); |
| 293 | 2408× | if due && self.instance.is_some() { |
| 294 | 0× | let cp = self.checkpoint_event(&mut h); |
| 295 | 0× | insert(&mut tx, &cp).await?; |
| 296 | 0× | projections::apply(&mut tx, &cp).await?; |
| 297 | 0× | out.push(cp); |
| 298 | 2408× | } |
| 299 | 2408× | tx.commit().await?; |
| 300 | 2408× | *head = h; |
| 301 | 2408× | drop(head); |
| 302 | 2408× | let out: Vec<Arc<Event>> = out.into_iter().map(Arc::new).collect(); |
| 303 | 12262× | for e in &out { |
| 304 | 12262× | let _ = self.bus.send(e.clone()); |
| 305 | 12262× | } |
| 306 | 2408× | Ok(out) |
| 307 | 2408× | } |
| 308 | |
|
| 309 | | |
| 310 | | |
| 311 | 140× | async fn checkpoint(&self) -> Result<Option<Arc<Event>>> { |
| 312 | 140× | let Some(_) = &self.instance else { return Ok(None) }; |
| 313 | 140× | let mut head = self.head.lock().await; |
| 314 | 140× | if head.since_checkpoint == 0 { |
| 315 | 4× | return Ok(None); |
| 316 | 136× | } |
| 317 | 136× | let mut h = *head; |
| 318 | 136× | let cp = self.checkpoint_event(&mut h); |
| 319 | 136× | let mut tx = self.pool.begin().await?; |
| 320 | 136× | insert(&mut tx, &cp).await?; |
| 321 | 136× | projections::apply(&mut tx, &cp).await?; |
| 322 | 136× | tx.commit().await?; |
| 323 | 136× | *head = h; |
| 324 | 136× | drop(head); |
| 325 | 136× | let cp = Arc::new(cp); |
| 326 | 136× | let _ = self.bus.send(cp.clone()); |
| 327 | 136× | Ok(Some(cp)) |
| 328 | 140× | } |
| 329 | |
|
| 330 | 12398× | fn chain(&self, h: &mut Head, n: NewEvent) -> Event { |
| 331 | 12398× | let at_ms = ids::now_ms(); |
| 332 | 12398× | let mut e = Event { |
| 333 | 12398× | seq: h.seq + 1, |
| 334 | 12398× | id: ids::ulid(), |
| 335 | 12398× | at_ms, |
| 336 | 12398× | hlc: self.clock.tick(at_ms), |
| 337 | 12398× | prev: h.hash, |
| 338 | 12398× | hash: [0; 32], |
| 339 | 12398× | auth: n.auth, |
| 340 | 12398× | kind: n.kind, |
| 341 | 12398× | actor: n.actor, |
| 342 | 12398× | session: n.session, |
| 343 | 12398× | project: n.project, |
| 344 | 12398× | subject: n.subject, |
| 345 | 12398× | payload: Some(canonical_json(&n.payload)), |
| 346 | 12398× | trace: n.trace, |
| 347 | 12398× | }; |
| 348 | 12398× | e.hash = e.computed_hash(); |
| 349 | 12398× | h.seq = e.seq; |
| 350 | 12398× | h.hash = e.hash; |
| 351 | 12398× | e |
| 352 | 12398× | } |
| 353 | |
|
| 354 | | |
| 355 | | |
| 356 | 180× | fn checkpoint_event(&self, h: &mut Head) -> Event { |
| 357 | 180× | let instance = self.instance.as_ref().expect("checked by callers"); |
| 358 | 180× | let (seq, hash, count) = (h.seq, h.hash, h.since_checkpoint); |
| 359 | 180× | let sig = instance.sign(&checkpoint_message(seq, &hash)); |
| 360 | 180× | let n = NewEvent::system("ledger.checkpoint", serde_json::json!({ "seq": seq, "hash": hex::encode(hash), "count": count })).by( |
| 361 | | "system", |
| 362 | 180× | Auth { key: Some(instance.key.0), method: Method::Signature, assurance: 2, proof: Some(crate::cbor::encode(&sig.to_value())) }, |
| 363 | | ); |
| 364 | 180× | let e = self.chain(h, n); |
| 365 | 180× | h.since_checkpoint = 0; |
| 366 | 180× | h.oldest_unchecked_ms = None; |
| 367 | 180× | e |
| 368 | 180× | } |
| 369 | |
|
| 370 | | |
| 371 | 5× | async fn rebuild(&self) -> Result<()> { |
| 372 | 5× | let _head = self.head.lock().await; |
| 373 | 5× | let mut tx = self.pool.begin().await?; |
| 374 | 5× | replay(&mut tx).await?; |
| 375 | 5× | tx.commit().await?; |
| 376 | 5× | Ok(()) |
| 377 | 5× | } |
| 378 | |
|
| 379 | | |
| 380 | | |
| 381 | 13× | async fn check(&self) -> Result<Vec<String>> { |
| 382 | 13× | let _head = self.head.lock().await; |
| 383 | 13× | let mut tx = self.pool.begin().await?; |
| 384 | 13× | let mut before = Vec::new(); |
| 385 | 169× | for t in projections::TABLES { |
| 386 | 169× | before.push(dump(&mut tx, t).await?); |
| 387 | | } |
| 388 | 13× | replay(&mut tx).await?; |
| 389 | 13× | let mut differ = Vec::new(); |
| 390 | 169× | for (t, b) in projections::TABLES.iter().zip(before) { |
| 391 | 169× | if dump(&mut tx, t).await? != b { |
| 392 | 0× | differ.push(t.to_string()); |
| 393 | 169× | } |
| 394 | | } |
| 395 | 13× | tx.rollback().await?; |
| 396 | 13× | Ok(differ) |
| 397 | 13× | } |
| 398 | | } |
| 399 | |
|
| 400 | |
|
| 401 | 320× | pub fn checkpoint_message(seq: u64, hash: &[u8; 32]) -> Vec<u8> { |
| 402 | 320× | let mut m = CHECKPOINT_DOMAIN.to_vec(); |
| 403 | 320× | m.extend_from_slice(hash); |
| 404 | 320× | m.extend_from_slice(&seq.to_be_bytes()); |
| 405 | 320× | m |
| 406 | 320× | } |
| 407 | |
|
| 408 | 140× | fn check_checkpoint(e: &Event, prev: [u8; 32], instance: Option<&Identity>) -> std::result::Result<(), String> { |
| 409 | 140× | let p = e.json(); |
| 410 | 140× | let seq = p.get("seq").and_then(|s| s.as_u64()).ok_or("no seq")?; |
| 411 | 140× | let hash: [u8; 32] = |
| 412 | 140× | p.get("hash").and_then(|h| h.as_str()).and_then(|h| hex::decode(h).ok()).and_then(|b| b.try_into().ok()).ok_or("no hash")?; |
| 413 | 140× | if seq + 1 != e.seq || hash != prev { |
| 414 | 1× | return Err("names a head other than the event before it".into()); |
| 415 | 139× | } |
| 416 | 139× | let instance = instance.ok_or("no instance identity precedes it")?; |
| 417 | 139× | if e.auth.key != Some(instance.hash().0) { |
| 418 | 0× | return Err("signed by a key other than the instance's".into()); |
| 419 | 139× | } |
| 420 | 139× | let proof = e.auth.proof.as_deref().ok_or("unsigned")?; |
| 421 | 139× | let sig = crate::cbor::decode(proof).ok().and_then(|v| Signature::from_value(&v).ok()).ok_or("malformed signature")?; |
| 422 | 139× | crate::identity::verify(instance, &sig, &checkpoint_message(seq, &hash)).map_err(|e| format!("signature does not verify: {e}")) |
| 423 | 140× | } |
| 424 | |
|
| 425 | |
|
| 426 | |
|
| 427 | |
|
| 428 | |
|
| 429 | | pub trait KeyResolver: Send { |
| 430 | | fn check(&mut self, e: &Event) -> std::result::Result<(), String>; |
| 431 | | } |
| 432 | |
|
| 433 | |
|
| 434 | | pub struct NoKeys; |
| 435 | | impl KeyResolver for NoKeys { |
| 436 | 10641× | fn check(&mut self, e: &Event) -> std::result::Result<(), String> { |
| 437 | 10641× | match e.auth.method { |
| 438 | 0× | Method::Signature | Method::Passkey => Err("no identity is known for its key".into()), |
| 439 | 10641× | _ => Ok(()), |
| 440 | | } |
| 441 | 10641× | } |
| 442 | | } |
| 443 | |
|
| 444 | |
|
| 445 | | #[derive(Clone, Debug, Default, serde::Serialize)] |
| 446 | | pub struct Verification { |
| 447 | | pub events: u64, |
| 448 | | pub checkpoints: u64, |
| 449 | | pub last_checkpoint: Option<u64>, |
| 450 | | |
| 451 | | pub uncovered: u64, |
| 452 | | #[serde(serialize_with = "ser_hex")] |
| 453 | | pub head: [u8; 32], |
| 454 | | pub findings: Vec<Finding>, |
| 455 | | } |
| 456 | |
|
| 457 | 22× | fn ser_hex<S: serde::Serializer>(h: &[u8; 32], s: S) -> std::result::Result<S::Ok, S::Error> { |
| 458 | 22× | s.serialize_str(&hex::encode(h)) |
| 459 | 22× | } |
| 460 | |
|
| 461 | | impl Verification { |
| 462 | 9× | pub fn clean(&self) -> bool { |
| 463 | 9× | self.findings.is_empty() |
| 464 | 9× | } |
| 465 | 17× | fn finding(&mut self, seq: u64, problem: Problem, detail: String) { |
| 466 | 17× | self.findings.push(Finding { seq, problem, detail }); |
| 467 | 17× | } |
| 468 | | } |
| 469 | |
|
| 470 | |
|
| 471 | | #[derive(Clone, Debug, serde::Serialize)] |
| 472 | | pub struct Finding { |
| 473 | | pub seq: u64, |
| 474 | | pub problem: Problem, |
| 475 | | pub detail: String, |
| 476 | | } |
| 477 | |
|
| 478 | | #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] |
| 479 | | #[serde(rename_all = "snake_case")] |
| 480 | | pub enum Problem { |
| 481 | | Gap, |
| 482 | | ChainBreak, |
| 483 | | HashMismatch, |
| 484 | | BadCheckpoint, |
| 485 | | BadProof, |
| 486 | | } |
| 487 | |
|
| 488 | | const SELECT_EVENT: &str = "SELECT seq, id, at_ms, hlc_wall, hlc_logical, prev, hash, auth_key, auth_method, auth_assurance, auth_proof, kind, actor, session, project, subject, payload, trace FROM events"; |
| 489 | |
|
| 490 | 12398× | async fn insert(conn: &mut sqlx::SqliteConnection, e: &Event) -> sqlx::Result<()> { |
| 491 | 12398× | sqlx::query( |
| 492 | 12398× | "INSERT INTO events (seq, id, at_ms, hlc_wall, hlc_logical, prev, hash, auth_key, auth_method, auth_assurance, auth_proof, kind, actor, session, project, subject, payload, trace) \ |
| 493 | 12398× | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", |
| 494 | | ) |
| 495 | 12398× | .bind(e.seq as i64) |
| 496 | 12398× | .bind(&e.id) |
| 497 | 12398× | .bind(e.at_ms as i64) |
| 498 | 12398× | .bind(e.hlc.wall_ms as i64) |
| 499 | 12398× | .bind(e.hlc.logical as i64) |
| 500 | 12398× | .bind(e.prev.to_vec()) |
| 501 | 12398× | .bind(e.hash.to_vec()) |
| 502 | 12398× | .bind(e.auth.key.map(|k| k.to_vec())) |
| 503 | 12398× | .bind(e.auth.method as i64) |
| 504 | 12398× | .bind(e.auth.assurance as i64) |
| 505 | 12398× | .bind(e.auth.proof.clone()) |
| 506 | 12398× | .bind(&e.kind) |
| 507 | 12398× | .bind(&e.actor) |
| 508 | 12398× | .bind(&e.session) |
| 509 | 12398× | .bind(&e.project) |
| 510 | 12398× | .bind(&e.subject) |
| 511 | 12398× | .bind(e.payload.clone()) |
| 512 | 12398× | .bind(&e.trace) |
| 513 | 12398× | .execute(conn) |
| 514 | 12398× | .await?; |
| 515 | 12398× | Ok(()) |
| 516 | 12398× | } |
| 517 | |
|
| 518 | 42673× | fn row_to_event(r: &SqliteRow) -> Result<Event> { |
| 519 | | Ok(Event { |
| 520 | 42673× | seq: r.get::<i64, _>(0) as u64, |
| 521 | 42673× | id: r.get(1), |
| 522 | 42673× | at_ms: r.get::<i64, _>(2) as u64, |
| 523 | 42673× | hlc: Hlc { wall_ms: r.get::<i64, _>(3) as u64, logical: r.get::<i64, _>(4) as u32 }, |
| 524 | 42673× | prev: to32(r.get(5))?, |
| 525 | 42673× | hash: to32(r.get(6))?, |
| 526 | | auth: Auth { |
| 527 | 42673× | key: r.get::<Option<Vec<u8>>, _>(7).map(to32).transpose()?, |
| 528 | 42673× | method: Method::from_u8(r.get::<i64, _>(8) as u8).context("unknown auth method")?, |
| 529 | 42673× | assurance: r.get::<i64, _>(9) as u8, |
| 530 | 42673× | proof: r.get(10), |
| 531 | | }, |
| 532 | 42673× | kind: r.get(11), |
| 533 | 42673× | actor: r.get(12), |
| 534 | 42673× | session: r.get(13), |
| 535 | 42673× | project: r.get(14), |
| 536 | 42673× | subject: r.get(15), |
| 537 | 42673× | payload: r.get(16), |
| 538 | 42673× | trace: r.get(17), |
| 539 | | }) |
| 540 | 42673× | } |
| 541 | |
|
| 542 | 87149× | fn to32(v: Vec<u8>) -> Result<[u8; 32]> { |
| 543 | 87149× | v.try_into().map_err(|_| anyhow::anyhow!("a stored hash is not 32 bytes")) |
| 544 | 87149× | } |
| 545 | |
|
| 546 | 18× | async fn replay(conn: &mut sqlx::SqliteConnection) -> Result<()> { |
| 547 | 234× | for t in projections::TABLES { |
| 548 | 234× | sqlx::query(&format!("DELETE FROM {t}")).execute(&mut *conn).await?; |
| 549 | | } |
| 550 | 18× | let mut from = 1i64; |
| 551 | | loop { |
| 552 | 51× | let rows = sqlx::query(&format!("{SELECT_EVENT} WHERE seq >= ? ORDER BY seq LIMIT 2000")).bind(from).fetch_all(&mut *conn).await?; |
| 553 | 51× | if rows.is_empty() { |
| 554 | 18× | break; |
| 555 | 33× | } |
| 556 | 30505× | for r in &rows { |
| 557 | 30505× | let e = row_to_event(r)?; |
| 558 | 30505× | projections::apply(conn, &e).await?; |
| 559 | 30505× | from = e.seq as i64 + 1; |
| 560 | | } |
| 561 | | } |
| 562 | 18× | Ok(()) |
| 563 | 18× | } |
| 564 | |
|
| 565 | 338× | async fn dump(conn: &mut sqlx::SqliteConnection, table: &str) -> Result<Vec<String>> { |
| 566 | | |
| 567 | 338× | let rows = sqlx::query(&format!("SELECT * FROM {table} ORDER BY 1, 2")).fetch_all(&mut *conn).await?; |
| 568 | 338× | Ok(rows |
| 569 | 338× | .iter() |
| 570 | 538× | .map(|r| { |
| 571 | 538× | (0..r.columns().len()) |
| 572 | 2816× | .map(|i| match r.try_get::<Option<String>, _>(i) { |
| 573 | 1980× | Ok(v) => format!("{v:?}"), |
| 574 | 836× | Err(_) => match r.try_get::<Option<i64>, _>(i) { |
| 575 | 738× | Ok(n) => format!("{n:?}"), |
| 576 | 98× | Err(_) => format!("{:?}", r.try_get::<Option<Vec<u8>>, _>(i).ok().flatten()), |
| 577 | | }, |
| 578 | 2816× | }) |
| 579 | 538× | .collect::<Vec<_>>() |
| 580 | 538× | .join("|") |
| 581 | 538× | }) |
| 582 | 338× | .collect()) |
| 583 | 338× | } |
| 584 | |
|
| 585 | |
|
| 586 | 0× | pub fn find_identity<'a>(identities: &'a [Identity], key: &KeyId) -> Option<&'a Identity> { |
| 587 | 0× | identities.iter().find(|i| &i.hash() == key) |
| 588 | 0× | } |