OXYGENHUB

Coverage / crates/spectra-core/src/ledger.rs 95.0%

384 of 404 lines run, 104 of 232 functions · run #11 on main · run missed

1
//! The ledger (spec §9, §9.5): the append-only, hash-chained event log in
2
//! SQLite, its checkpoints signed by the instance key, its verification,
3
//! and the bus that tells everything else what was appended.
4
//!
5
//! One writer at a time (a lock around the chain head and SQLite's own),
6
//! so `seq` is gap-free and every `prev` is the hash of the event before.
7
//! Readers never wait for it (WAL).
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
/// Events between checkpoints (spec §9.5).
23
pub const CHECKPOINT_EVERY: u64 = 256;
24
/// The longest a written event waits for a checkpoint (spec §9.5).
25
pub const CHECKPOINT_INTERVAL_MS: u64 = 10 * 60 * 1000;
26
27
const CHECKPOINT_DOMAIN: &[u8] = b"spectra1 checkpoint ";
28
29
/// The ledger: the database, the chain head, the clock and the bus.
30
pub struct Ledger {
31
    pool: SqlitePool,
32
    /// Held for as long as this ledger may append: one writer per data
33
    /// directory, so the chain never forks (`None` when opened read-only).
34
    _lock: Option<std::fs::File>,
35
    head: Mutex<Head>,
36
    clock: Clock,
37
    bus: broadcast::Sender<Arc<Event>>,
38
    instance: Option<KeyPair>,
39
}
40
41
#[derive(Clone, Copy, Debug)]
42
struct Head {
43
    seq: u64,
44
    hash: [u8; 32],
45
    since_checkpoint: u64,
46
    oldest_unchecked_ms: Option<u64>,
47
}
48
49
impl Ledger {
50
    /// Open (or create) `path`, apply migrations, and refuse a ledger
51
    /// written by a newer format. Checkpoints are signed with `instance`;
52
    /// without one (tests, read-only tools) none are made.
5384×
    pub async fn open(path: &Path, instance: Option<KeyPair>) -> Result<Ledger> {
5484×
        let lock_path = path.with_extension("lock");
5584×
        let lock = std::fs::OpenOptions::new()
5684×
            .create(true)
5784×
            .truncate(false)
5884×
            .write(true)
5984×
            .open(&lock_path)
6084×
            .with_context(|| format!("opening {}", lock_path.display()))?;
6184×
        match lock.try_lock() {
6283×
            Ok(()) => {}
63
            Err(std::fs::TryLockError::WouldBlock) => {
641×
                bail!("another process is writing to {} (is `spectra serve` running? stop it, or do this through it)", path.display())
65
            }
660×
            Err(std::fs::TryLockError::Error(e)) => return Err(e).with_context(|| format!("locking {}", lock_path.display())),
67
        }
6883×
        let mut l = Ledger::open_unlocked(path, instance).await?;
6982×
        l._lock = Some(lock);
7082×
        Ok(l)
7184×
    }
72
73
    /// Open for reading and verifying only, while another process may be
74
    /// writing: no lock, and `append` must not be called.
7535×
    pub async fn open_read_only(path: &Path) -> Result<Ledger> {
7635×
        Ledger::open_unlocked(path, None).await
7735×
    }
78
79118×
    async fn open_unlocked(path: &Path, instance: Option<KeyPair>) -> Result<Ledger> {
80118×
        let opts = SqliteConnectOptions::new()
81118×
            .filename(path)
82118×
            .create_if_missing(true)
83118×
            .journal_mode(SqliteJournalMode::Wal)
84118×
            .synchronous(SqliteSynchronous::Normal)
85118×
            .foreign_keys(true)
86118×
            .busy_timeout(std::time::Duration::from_secs(10));
87118×
        let pool =
88118×
            SqlitePoolOptions::new().max_connections(8).connect_with(opts).await.with_context(|| format!("opening {}", path.display()))?;
89118×
        sqlx::migrate!("./migrations").run(&pool).await.context("applying migrations")?;
90118×
        let format: Option<String> = sqlx::query_scalar("SELECT value FROM meta WHERE key = 'ledger_format'").fetch_optional(&pool).await?;
91118×
        match format.as_deref().map(str::parse::<u32>) {
92
            None => {
9336×
                sqlx::query("INSERT INTO meta (key, value) VALUES ('ledger_format', ?)")
9436×
                    .bind(crate::LEDGER_FORMAT.to_string())
9536×
                    .execute(&pool)
9636×
                    .await?;
97
            }
9882×
            Some(Ok(f)) if f <= crate::LEDGER_FORMAT => {}
99
            Some(_) => {
1001×
                bail!("{} was written by a newer Spectra (ledger format {}): upgrade spectra", path.display(), format.unwrap_or_default())
101
            }
102
        }
103117×
        let last =
104117×
            sqlx::query("SELECT seq, hash, hlc_wall, hlc_logical FROM events ORDER BY seq DESC LIMIT 1").fetch_optional(&pool).await?;
105117×
        let (seq, hash, hlc) = match last {
10681×
            Some(r) => (
10781×
                r.get::<i64, _>(0) as u64,
10881×
                to32(r.get::<Vec<u8>, _>(1))?,
10981×
                Hlc { wall_ms: r.get::<i64, _>(2) as u64, logical: r.get::<i64, _>(3) as u32 },
110
            ),
11136×
            None => (0, [0; 32], Hlc::default()),
112
        };
113117×
        let checkpoint_seq: i64 =
114117×
            sqlx::query_scalar("SELECT COALESCE(MAX(seq), 0) FROM events WHERE kind = 'ledger.checkpoint'").fetch_one(&pool).await?;
115117×
        let since = seq.saturating_sub(checkpoint_seq as u64);
116117×
        let (bus, _) = broadcast::channel(4096);
117117×
        Ok(Ledger {
118117×
            pool,
119117×
            _lock: None,
120117×
            head: Mutex::new(Head { seq, hash, since_checkpoint: since, oldest_unchecked_ms: (since > 0).then(ids::now_ms) }),
121117×
            clock: Clock::starting_at(hlc),
122117×
            bus,
123117×
            instance,
124117×
        })
125118×
    }
126
127
    /// The database, for projections' readers.
128294×
    pub fn pool(&self) -> &SqlitePool {
129294×
        &self.pool
130294×
    }
131
132
    /// Every appended event, as it commits.
13313×
    pub fn subscribe(&self) -> broadcast::Receiver<Arc<Event>> {
13413×
        self.bus.subscribe()
13513×
    }
136
137
    /// The instance key, when this ledger signs checkpoints.
1380×
    pub fn instance(&self) -> Option<&KeyPair> {
1390×
        self.instance.as_ref()
1400×
    }
141
142
    /// The newest event's sequence number and hash.
14332×
    pub async fn head(&self) -> (u64, [u8; 32]) {
14432×
        let h = self.head.lock().await;
14532×
        (h.seq, h.hash)
14632×
    }
147
148
    /// Append `events` in one transaction, chained and projected, then
149
    /// publish them; a checkpoint follows when one is due.
1501946×
    pub async fn append(&self, events: Vec<NewEvent>) -> Result<Vec<Arc<Event>>> {
1511946×
        let mut head = self.head.lock().await;
1521946×
        let mut tx = self.pool.begin().await?;
1531946×
        let mut out = Vec::with_capacity(events.len() + 1);
1541946×
        let mut h = *head;
15511671×
        for n in events {
15611671×
            let e = self.chain(&mut h, n);
15711671×
            insert(&mut tx, &e).await?;
15811671×
            projections::apply(&mut tx, &e).await?;
15911671×
            h.since_checkpoint += 1;
16011671×
            h.oldest_unchecked_ms.get_or_insert(e.at_ms);
16111671×
            out.push(e);
162
            // Every 256 events exactly, even inside a large batch.
16311671×
            if h.since_checkpoint >= CHECKPOINT_EVERY && self.instance.is_some() {
16443×
                let cp = self.checkpoint_event(&mut h);
16543×
                insert(&mut tx, &cp).await?;
16643×
                projections::apply(&mut tx, &cp).await?;
16743×
                out.push(cp);
16811628×
            }
169
        }
1701946×
        let due = h.oldest_unchecked_ms.is_some_and(|t| ids::now_ms().saturating_sub(t) >= CHECKPOINT_INTERVAL_MS);
1711946×
        if due && self.instance.is_some() {
1720×
            let cp = self.checkpoint_event(&mut h);
1730×
            insert(&mut tx, &cp).await?;
1740×
            projections::apply(&mut tx, &cp).await?;
1750×
            out.push(cp);
1761946×
        }
1771946×
        tx.commit().await?;
1781946×
        *head = h;
1791946×
        drop(head);
1801946×
        let out: Vec<Arc<Event>> = out.into_iter().map(Arc::new).collect();
18111714×
        for e in &out {
18211714×
            let _ = self.bus.send(e.clone());
18311714×
        }
1841946×
        Ok(out)
1851946×
    }
186
187
    /// Append one event.
1881481×
    pub async fn append_one(&self, e: NewEvent) -> Result<Arc<Event>> {
1891481×
        Ok(self.append(vec![e]).await?.remove(0))
1901481×
    }
191
192
    /// Write a checkpoint now if any event is not covered by one (at
193
    /// shutdown, and from the ten-minute timer).
19456×
    pub async fn checkpoint(&self) -> Result<Option<Arc<Event>>> {
19556×
        let Some(_) = &self.instance else { return Ok(None) };
19656×
        let mut head = self.head.lock().await;
19756×
        if head.since_checkpoint == 0 {
1981×
            return Ok(None);
19955×
        }
20055×
        let mut h = *head;
20155×
        let cp = self.checkpoint_event(&mut h);
20255×
        let mut tx = self.pool.begin().await?;
20355×
        insert(&mut tx, &cp).await?;
20455×
        projections::apply(&mut tx, &cp).await?;
20555×
        tx.commit().await?;
20655×
        *head = h;
20755×
        drop(head);
20855×
        let cp = Arc::new(cp);
20955×
        let _ = self.bus.send(cp.clone());
21055×
        Ok(Some(cp))
21156×
    }
212
21311769×
    fn chain(&self, h: &mut Head, n: NewEvent) -> Event {
21411769×
        let at_ms = ids::now_ms();
21511769×
        let mut e = Event {
21611769×
            seq: h.seq + 1,
21711769×
            id: ids::ulid(),
21811769×
            at_ms,
21911769×
            hlc: self.clock.tick(at_ms),
22011769×
            prev: h.hash,
22111769×
            hash: [0; 32],
22211769×
            auth: n.auth,
22311769×
            kind: n.kind,
22411769×
            actor: n.actor,
22511769×
            session: n.session,
22611769×
            project: n.project,
22711769×
            subject: n.subject,
22811769×
            payload: Some(canonical_json(&n.payload)),
22911769×
            trace: n.trace,
23011769×
        };
23111769×
        e.hash = e.computed_hash();
23211769×
        h.seq = e.seq;
23311769×
        h.hash = e.hash;
23411769×
        e
23511769×
    }
236
237
    /// A checkpoint naming the head `h` holds, signed by the instance key
238
    /// over `"spectra1 checkpoint " || hash || seq` (spec §9.5).
23998×
    fn checkpoint_event(&self, h: &mut Head) -> Event {
24098×
        let instance = self.instance.as_ref().expect("checked by callers");
24198×
        let (seq, hash, count) = (h.seq, h.hash, h.since_checkpoint);
24298×
        let sig = instance.sign(&checkpoint_message(seq, &hash));
24398×
        let n = NewEvent::system("ledger.checkpoint", serde_json::json!({ "seq": seq, "hash": hex::encode(hash), "count": count })).by(
244
            "system",
24598×
            Auth {
24698×
                key: Some(instance.key.0),
24798×
                method: Method::Signature,
24898×
                assurance: 2,
24998×
                proof: Some(oxy_core::cbor::encode(&sig.to_value())),
25098×
            },
251
        );
25298×
        let e = self.chain(h, n);
25398×
        h.since_checkpoint = 0;
25498×
        h.oldest_unchecked_ms = None;
25598×
        e
25698×
    }
257
258
    /// Events from `from` (inclusive), oldest first, at most `limit`.
25964×
    pub async fn events(&self, from: u64, limit: u32) -> Result<Vec<Event>> {
26064×
        let rows = sqlx::query(&format!("{SELECT_EVENT} WHERE seq >= ? ORDER BY seq LIMIT ?"))
26164×
            .bind(from as i64)
26264×
            .bind(limit as i64)
26364×
            .fetch_all(&self.pool)
26464×
            .await?;
26564×
        rows.iter().map(row_to_event).collect()
26664×
    }
267
268
    /// Events of the given kinds from `from` (inclusive), oldest first.
269170×
    pub async fn events_of_kinds(&self, from: u64, kinds: &[&str], limit: u32) -> Result<Vec<Event>> {
270170×
        let marks = vec!["?"; kinds.len()].join(",");
271170×
        let sql = format!("{SELECT_EVENT} WHERE seq >= ? AND kind IN ({marks}) ORDER BY seq LIMIT ?");
272170×
        let mut q = sqlx::query(&sql).bind(from as i64);
273850×
        for k in kinds {
274850×
            q = q.bind(*k);
275850×
        }
276170×
        let rows = q.bind(limit as i64).fetch_all(&self.pool).await?;
277169×
        rows.iter().map(row_to_event).collect()
278169×
    }
279
280
    /// Recompute the chain and check every checkpoint and every proof it
281
    /// can, from the ledger alone (spec §9.5, `spectra admin verify`).
28229×
    pub async fn verify(&self, keys: &mut dyn KeyResolver) -> Result<Verification> {
28329×
        let mut v = Verification::default();
28429×
        let mut prev = [0u8; 32];
28529×
        let mut expect = 1u64;
28629×
        let mut instance: Option<Identity> = None;
28729×
        let mut from = 1;
288
        loop {
28963×
            let batch = self.events(from, 2_000).await?;
29063×
            if batch.is_empty() {
29129×
                break;
29234×
            }
29310722×
            for e in &batch {
29410722×
                v.events += 1;
29510722×
                if e.seq != expect {
2961×
                    v.finding(e.seq, Problem::Gap, format!("expected event {expect}"));
2971×
                    expect = e.seq;
29810721×
                }
29910722×
                if e.prev != prev {
3002×
                    v.finding(e.seq, Problem::ChainBreak, "prev is not the hash of the event before".into());
30110720×
                }
30210722×
                if e.payload.is_some() && e.computed_hash() != e.hash {
3032×
                    v.finding(e.seq, Problem::HashMismatch, "the event's fields do not give its hash: it was edited".into());
30410720×
                }
30510722×
                if e.kind == "identity.instance" {
30629×
                    instance = e
30729×
                        .json()
30829×
                        .get("identity")
30929×
                        .and_then(|h| h.as_str())
31029×
                        .and_then(|h| hex::decode(h).ok())
31129×
                        .and_then(|b| Identity::decode(&b).ok());
31210693×
                }
31310722×
                if e.kind == "ledger.checkpoint" {
31477×
                    v.checkpoints += 1;
31577×
                    match check_checkpoint(e, prev, instance.as_ref()) {
31675×
                        Ok(()) => v.last_checkpoint = Some(e.seq),
3172×
                        Err(why) => v.finding(e.seq, Problem::BadCheckpoint, why),
318
                    }
31910645×
                } else if let Err(why) = keys.check(e) {
32010×
                    v.finding(e.seq, Problem::BadProof, why);
32110635×
                }
32210722×
                prev = e.hash;
32310722×
                expect += 1;
324
            }
32534×
            from = batch.last().map(|e| e.seq + 1).unwrap_or(from);
326
        }
32729×
        v.head = prev;
32829×
        v.uncovered = v.events.saturating_sub(v.last_checkpoint.unwrap_or(0));
32929×
        Ok(v)
33029×
    }
331
332
    /// Clear every projection and replay the ledger into them.
3332×
    pub async fn rebuild(&self) -> Result<()> {
3342×
        let _head = self.head.lock().await;
3352×
        let mut tx = self.pool.begin().await?;
3362×
        replay(&mut tx).await?;
3372×
        tx.commit().await?;
3382×
        Ok(())
3392×
    }
340
341
    /// Whether the projections equal a rebuild from the ledger: rebuild in
342
    /// a transaction, compare, and roll back. Returns the tables that differ.
34311×
    pub async fn check(&self) -> Result<Vec<String>> {
34411×
        let _head = self.head.lock().await;
34511×
        let mut tx = self.pool.begin().await?;
34611×
        let mut before = Vec::new();
347132×
        for t in projections::TABLES {
348132×
            before.push(dump(&mut tx, t).await?);
349
        }
35011×
        replay(&mut tx).await?;
35111×
        let mut differ = Vec::new();
352132×
        for (t, b) in projections::TABLES.iter().zip(before) {
353132×
            if dump(&mut tx, t).await? != b {
3540×
                differ.push(t.to_string());
355132×
            }
356
        }
35711×
        tx.rollback().await?;
35811×
        Ok(differ)
35911×
    }
360
}
361
362
/// `"spectra1 checkpoint " || hash || seq (8 bytes, big-endian)`.
363175×
pub fn checkpoint_message(seq: u64, hash: &[u8; 32]) -> Vec<u8> {
364175×
    let mut m = CHECKPOINT_DOMAIN.to_vec();
365175×
    m.extend_from_slice(hash);
366175×
    m.extend_from_slice(&seq.to_be_bytes());
367175×
    m
368175×
}
369
37077×
fn check_checkpoint(e: &Event, prev: [u8; 32], instance: Option<&Identity>) -> std::result::Result<(), String> {
37177×
    let p = e.json();
37277×
    let seq = p.get("seq").and_then(|s| s.as_u64()).ok_or("no seq")?;
37377×
    let hash: [u8; 32] =
37477×
        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")?;
37577×
    if seq + 1 != e.seq || hash != prev {
3761×
        return Err("names a head other than the event before it".into());
37776×
    }
37876×
    let instance = instance.ok_or("no instance identity precedes it")?;
37976×
    if e.auth.key != Some(instance.hash().0) {
3800×
        return Err("signed by a key other than the instance's".into());
38176×
    }
38276×
    let proof = e.auth.proof.as_deref().ok_or("unsigned")?;
38376×
    let sig = oxy_core::cbor::decode(proof).ok().and_then(|v| Signature::from_value(&v).ok()).ok_or("malformed signature")?;
38476×
    oxy_core::identity::verify_challenge(instance, &sig, &checkpoint_message(seq, &hash))
38576×
        .map_err(|e| format!("signature does not verify: {e}"))
38677×
}
387
388
/// Checks the proof of a signed event: the identities and delegations that
389
/// say whose key it was (spec §8.5). Principals supply it (`principals`).
390
/// It is given every event but checkpoints, in order, so it can follow
391
/// identities as they are recorded.
392
pub trait KeyResolver: Send {
393
    fn check(&mut self, e: &Event) -> std::result::Result<(), String>;
394
}
395
396
/// A resolver that knows no keys: every signed event is reported.
397
pub struct NoKeys;
398
impl KeyResolver for NoKeys {
39910339×
    fn check(&mut self, e: &Event) -> std::result::Result<(), String> {
40010339×
        match e.auth.method {
4010×
            Method::Signature | Method::Passkey => Err("no identity is known for its key".into()),
40210339×
            _ => Ok(()),
403
        }
40410339×
    }
405
}
406
407
/// What `verify` found.
408
#[derive(Clone, Debug, Default, serde::Serialize)]
409
pub struct Verification {
410
    pub events: u64,
411
    pub checkpoints: u64,
412
    pub last_checkpoint: Option<u64>,
413
    /// Events after the last good checkpoint: chained, not yet signed.
414
    pub uncovered: u64,
415
    #[serde(serialize_with = "ser_hex")]
416
    pub head: [u8; 32],
417
    pub findings: Vec<Finding>,
418
}
419
4207×
fn ser_hex<S: serde::Serializer>(h: &[u8; 32], s: S) -> std::result::Result<S::Ok, S::Error> {
4217×
    s.serialize_str(&hex::encode(h))
4227×
}
423
424
impl Verification {
42523×
    pub fn clean(&self) -> bool {
42623×
        self.findings.is_empty()
42723×
    }
42817×
    fn finding(&mut self, seq: u64, problem: Problem, detail: String) {
42917×
        self.findings.push(Finding { seq, problem, detail });
43017×
    }
431
}
432
433
/// One thing wrong with the ledger.
434
#[derive(Clone, Debug, serde::Serialize)]
435
pub struct Finding {
436
    pub seq: u64,
437
    pub problem: Problem,
438
    pub detail: String,
439
}
440
441
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
442
#[serde(rename_all = "snake_case")]
443
pub enum Problem {
444
    Gap,
445
    ChainBreak,
446
    HashMismatch,
447
    BadCheckpoint,
448
    BadProof,
449
}
450
451
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";
452
45311769×
async fn insert(conn: &mut sqlx::SqliteConnection, e: &Event) -> sqlx::Result<()> {
45411769×
    sqlx::query(
45511769×
        "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) \
45611769×
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
457
    )
45811769×
    .bind(e.seq as i64)
45911769×
    .bind(&e.id)
46011769×
    .bind(e.at_ms as i64)
46111769×
    .bind(e.hlc.wall_ms as i64)
46211769×
    .bind(e.hlc.logical as i64)
46311769×
    .bind(e.prev.to_vec())
46411769×
    .bind(e.hash.to_vec())
46511769×
    .bind(e.auth.key.map(|k| k.to_vec()))
46611769×
    .bind(e.auth.method as i64)
46711769×
    .bind(e.auth.assurance as i64)
46811769×
    .bind(e.auth.proof.clone())
46911769×
    .bind(&e.kind)
47011769×
    .bind(&e.actor)
47111769×
    .bind(&e.session)
47211769×
    .bind(&e.project)
47311769×
    .bind(&e.subject)
47411769×
    .bind(e.payload.clone())
47511769×
    .bind(&e.trace)
47611769×
    .execute(conn)
47711769×
    .await?;
47811769×
    Ok(())
47911769×
}
480
48141406×
fn row_to_event(r: &SqliteRow) -> Result<Event> {
482
    Ok(Event {
48341406×
        seq: r.get::<i64, _>(0) as u64,
48441406×
        id: r.get(1),
48541406×
        at_ms: r.get::<i64, _>(2) as u64,
48641406×
        hlc: Hlc { wall_ms: r.get::<i64, _>(3) as u64, logical: r.get::<i64, _>(4) as u32 },
48741406×
        prev: to32(r.get(5))?,
48841406×
        hash: to32(r.get(6))?,
489
        auth: Auth {
49041406×
            key: r.get::<Option<Vec<u8>>, _>(7).map(to32).transpose()?,
49141406×
            method: Method::from_u8(r.get::<i64, _>(8) as u8).context("unknown auth method")?,
49241406×
            assurance: r.get::<i64, _>(9) as u8,
49341406×
            proof: r.get(10),
494
        },
49541406×
        kind: r.get(11),
49641406×
        actor: r.get(12),
49741406×
        session: r.get(13),
49841406×
        project: r.get(14),
49941406×
        subject: r.get(15),
50041406×
        payload: r.get(16),
50141406×
        trace: r.get(17),
502
    })
50341406×
}
504
50583635×
fn to32(v: Vec<u8>) -> Result<[u8; 32]> {
50683635×
    v.try_into().map_err(|_| anyhow::anyhow!("a stored hash is not 32 bytes"))
50783635×
}
508
50913×
async fn replay(conn: &mut sqlx::SqliteConnection) -> Result<()> {
510156×
    for t in projections::TABLES {
511156×
        sqlx::query(&format!("DELETE FROM {t}")).execute(&mut *conn).await?;
512
    }
51313×
    let mut from = 1i64;
514
    loop {
51541×
        let rows = sqlx::query(&format!("{SELECT_EVENT} WHERE seq >= ? ORDER BY seq LIMIT 2000")).bind(from).fetch_all(&mut *conn).await?;
51641×
        if rows.is_empty() {
51713×
            break;
51828×
        }
51930358×
        for r in &rows {
52030358×
            let e = row_to_event(r)?;
52130358×
            projections::apply(conn, &e).await?;
52230358×
            from = e.seq as i64 + 1;
523
        }
524
    }
52513×
    Ok(())
52613×
}
527
528264×
async fn dump(conn: &mut sqlx::SqliteConnection, table: &str) -> Result<Vec<String>> {
529
    // Every column as text, rows in a fixed order: enough to compare.
530264×
    let rows = sqlx::query(&format!("SELECT * FROM {table} ORDER BY 1, 2")).fetch_all(&mut *conn).await?;
531264×
    Ok(rows
532264×
        .iter()
533426×
        .map(|r| {
534426×
            (0..r.columns().len())
5352264×
                .map(|i| match r.try_get::<Option<String>, _>(i) {
5361608×
                    Ok(v) => format!("{v:?}"),
537656×
                    Err(_) => match r.try_get::<Option<i64>, _>(i) {
538582×
                        Ok(n) => format!("{n:?}"),
53974×
                        Err(_) => format!("{:?}", r.try_get::<Option<Vec<u8>>, _>(i).ok().flatten()),
540
                    },
5412264×
                })
542426×
                .collect::<Vec<_>>()
543426×
                .join("|")
544426×
        })
545264×
        .collect())
546264×
}
547
548
/// The identity of `key` among `identities`, by KeyId.
5490×
pub fn find_identity<'a>(identities: &'a [Identity], key: &KeyId) -> Option<&'a Identity> {
5500×
    identities.iter().find(|i| &i.hash() == key)
5510×
}