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