atagen

vineyard

public
Star0
Watch0
25d14e49ab6861630869e2129d40652900bc5833
1
ssh://brewery@pls.lobotomise.me:33/atagen/vineyard.git
src/journal.rsRust1028 lines32.3 KB
1
use std::cmp::Ordering;
2
use std::collections::{BTreeSet, HashSet};
3
use std::error::Error;
4
use std::path::{Path, PathBuf};
5
use std::sync::Arc;
6
use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8
use anyhow::{Context as _, Result};
9
use nostr::event::{Event, EventId, Kind, Signature, Tags};
10
use nostr::filter::Filter;
11
use nostr::key::PublicKey;
12
use nostr::types::Timestamp;
13
use nostr_database::{DatabaseEventStatus, RejectedReason, SaveEventStatus};
14
use rusqlite::{
15
    Connection, OptionalExtension as _, Params, Row, Transaction, params, params_from_iter,
16
};
17
use tokio::sync::{Mutex, MutexGuard};
18
use tokio::task::spawn_blocking;
19
20
use crate::config::relay_url;
21
use crate::query::EventQuery;
22
23
const SCHEMA: &str = "
24
CREATE TABLE IF NOT EXISTS events(
25
    id BLOB PRIMARY KEY NOT NULL CHECK(length(id) = 32),
26
    pubkey BLOB NOT NULL CHECK(length(pubkey) = 32),
27
    created_at INTEGER NOT NULL CHECK(created_at > 0),
28
    kind INTEGER NOT NULL CHECK(kind BETWEEN 0 AND 65535),
29
    content TEXT NOT NULL,
30
    tags JSONB NOT NULL,
31
    sig BLOB NOT NULL CHECK(length(sig) = 64)
32
) WITHOUT ROWID;
33
34
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at DESC);
35
CREATE INDEX IF NOT EXISTS idx_events_pubkey_created_at ON events(pubkey, created_at DESC);
36
CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind_created_at
37
ON events(pubkey, kind, created_at DESC);
38
CREATE INDEX IF NOT EXISTS idx_events_kind_created_at ON events(kind, created_at DESC);
39
40
DROP INDEX IF EXISTS idx_events_pubkey;
41
DROP INDEX IF EXISTS idx_events_kind;
42
43
CREATE TABLE IF NOT EXISTS event_tags(
44
    event_id BLOB NOT NULL CHECK(length(event_id) = 32),
45
    tag_name TEXT NOT NULL CHECK(length(tag_name) = 1),
46
    tag_value TEXT NOT NULL,
47
    FOREIGN KEY(event_id) REFERENCES events(id) ON DELETE CASCADE,
48
    PRIMARY KEY(tag_name, tag_value, event_id)
49
);
50
51
CREATE INDEX IF NOT EXISTS idx_tags_event_id ON event_tags(event_id);
52
53
CREATE TABLE IF NOT EXISTS deleted_ids(
54
    event_id BLOB PRIMARY KEY NOT NULL CHECK(length(event_id) = 32)
55
) WITHOUT ROWID;
56
57
CREATE TABLE IF NOT EXISTS deleted_coordinates(
58
    pubkey BLOB NOT NULL CHECK(length(pubkey) = 32),
59
    kind INTEGER NOT NULL CHECK(kind BETWEEN 0 AND 65535),
60
    identifier TEXT NOT NULL,
61
    deleted_at INTEGER NOT NULL CHECK(deleted_at > 0),
62
    PRIMARY KEY(pubkey, kind, identifier)
63
) WITHOUT ROWID;
64
65
DROP TRIGGER IF EXISTS sanctum_deleted_after_insert;
66
DROP TABLE IF EXISTS sanctum_deleted_events;
67
68
CREATE TABLE IF NOT EXISTS vineyard_delivery(
69
    sequence INTEGER PRIMARY KEY AUTOINCREMENT,
70
    event_id BLOB NOT NULL UNIQUE REFERENCES events(id) ON DELETE CASCADE
71
);
72
73
CREATE TABLE IF NOT EXISTS vineyard_peers(
74
    peer TEXT PRIMARY KEY,
75
    delivered INTEGER NOT NULL DEFAULT 0
76
);
77
78
CREATE TABLE IF NOT EXISTS vineyard_checkpoints(
79
    peer TEXT,
80
    scope TEXT,
81
    until INTEGER NOT NULL,
82
    PRIMARY KEY(peer, scope)
83
);
84
85
CREATE TABLE IF NOT EXISTS vineyard_retries(
86
    peer TEXT,
87
    sequence INTEGER REFERENCES vineyard_delivery(sequence) ON DELETE CASCADE,
88
    retry_at INTEGER,
89
    error TEXT,
90
    PRIMARY KEY(peer, sequence)
91
);
92
93
CREATE TABLE IF NOT EXISTS vineyard_discovered_peers(
94
    signer BLOB,
95
    url TEXT,
96
    discovered_at INTEGER,
97
    PRIMARY KEY(signer, url)
98
) WITHOUT ROWID;
99
100
CREATE INDEX IF NOT EXISTS vineyard_retries_due
101
ON vineyard_retries(peer, retry_at, sequence);
102
103
CREATE INDEX IF NOT EXISTS vineyard_discovered_urls
104
ON vineyard_discovered_peers(url);
105
106
CREATE TRIGGER IF NOT EXISTS vineyard_delivery_after_insert
107
AFTER INSERT ON events
108
BEGIN
109
    INSERT OR IGNORE INTO vineyard_delivery(event_id) VALUES (NEW.id);
110
END;
111
112
";
113
114
const BACKFILL_DELIVERY: &str = "
115
INSERT INTO vineyard_delivery(event_id)
116
SELECT event.id
117
FROM events AS event
118
LEFT JOIN vineyard_delivery AS delivery ON delivery.event_id = event.id
119
WHERE delivery.event_id IS NULL
120
ORDER BY
121
    CASE WHEN event.kind = 30617 THEN 0 ELSE 1 END,
122
    event.created_at,
123
    event.id
124
";
125
126
const DISCOVERED_RELAY_TAGS: &str = "
127
SELECT event.pubkey, json_extract(tag.value, '$[1]'), event.created_at
128
FROM events AS event, json_each(event.tags) AS tag
129
WHERE event.kind = 10002
130
  AND json_extract(tag.value, '$[0]') = 'r'
131
  AND typeof(json_extract(tag.value, '$[1]')) = 'text'
132
ORDER BY event.created_at DESC, event.id
133
";
134
135
const DUE_DELIVERIES: &str = "
136
SELECT delivery.sequence, delivery.event_id
137
FROM vineyard_delivery AS delivery
138
JOIN vineyard_retries AS retry ON retry.sequence = delivery.sequence
139
WHERE retry.peer = ?1 AND retry.retry_at <= ?2
140
ORDER BY delivery.sequence
141
LIMIT ?3
142
";
143
144
const NEW_DELIVERIES: &str = "
145
SELECT sequence, event_id
146
FROM vineyard_delivery
147
WHERE sequence > ?1
148
ORDER BY sequence
149
LIMIT ?2
150
";
151
152
const REPOSITORIES: &str = "
153
SELECT lower(hex(event.pubkey)), tag.tag_value, event.tags
154
FROM events AS event
155
JOIN event_tags AS tag ON event.id = tag.event_id
156
WHERE event.kind = 30617 AND tag.tag_name = 'd'
157
ORDER BY 1, 2, event.created_at DESC, event.id
158
";
159
160
type Delivery = (u64, EventId);
161
162
#[derive(Debug, Clone)]
163
pub struct Journal {
164
    connection: Arc<Mutex<Connection>>,
165
    writes: Arc<Mutex<()>>,
166
}
167
168
struct StoredEvent {
169
    id: Vec<u8>,
170
    pubkey: Vec<u8>,
171
    created_at: i64,
172
    kind: i64,
173
    content: String,
174
    tags: String,
175
    signature: Vec<u8>,
176
}
177
178
impl Journal {
179
    pub async fn open(
180
        path: PathBuf,
181
        max_database_bytes: u64,
182
        max_discovered_peers: usize,
183
        excluded_peers: Vec<String>,
184
        writes: Arc<Mutex<()>>,
185
    ) -> Result<Self> {
186
        let connection = spawn_blocking(move || {
187
            open_connection(
188
                &path,
189
                max_database_bytes,
190
                max_discovered_peers,
191
                &excluded_peers,
192
            )
193
        })
194
        .await
195
        .context("journal task panicked")??;
196
197
        Ok(Self {
198
            connection: Arc::new(Mutex::new(connection)),
199
            writes,
200
        })
201
    }
202
203
    pub async fn pending(&self, peer: &str, limit: usize) -> Result<Vec<Delivery>> {
204
        let peer_url = peer.to_owned();
205
        self.run(move |connection| pending_deliveries(connection, &peer_url, limit))
206
            .await
207
    }
208
209
    pub async fn acknowledge(&self, peer: &str, sequence: u64) -> Result<()> {
210
        let peer_url = peer.to_owned();
211
        self.write(move |connection| acknowledge_delivery(connection, &peer_url, sequence))
212
            .await
213
    }
214
215
    pub async fn defer(&self, peer: &str, sequence: u64, error: &str, retry_at: u64) -> Result<()> {
216
        let peer_url = peer.to_owned();
217
        let message = error.chars().take(512).collect::<String>();
218
        self.write(move |connection| {
219
            defer_delivery(connection, &peer_url, sequence, &message, retry_at)
220
        })
221
        .await
222
    }
223
224
    pub async fn checkpoint(&self, peer: &str, scope: &str) -> Result<Option<u64>> {
225
        let peer_url = peer.to_owned();
226
        let scope_name = scope.to_owned();
227
        self.run(move |connection| read_checkpoint(connection, &peer_url, &scope_name))
228
            .await
229
    }
230
231
    pub async fn advance(&self, peer: &str, scope: &str, until: u64) -> Result<()> {
232
        let peer_url = peer.to_owned();
233
        let scope_name = scope.to_owned();
234
        self.write(move |connection| write_checkpoint(connection, &peer_url, &scope_name, until))
235
            .await
236
    }
237
238
    pub async fn repositories(&self, limit: usize) -> Result<Vec<(String, String)>> {
239
        self.run(move |connection| list_repositories(connection, limit))
240
            .await
241
    }
242
243
    pub async fn healthy(&self) -> bool {
244
        self.run(|connection| Ok(connection.query_row("SELECT 1", [], |_| Ok(()))?))
245
            .await
246
            .is_ok()
247
    }
248
249
    pub async fn save_locked(
250
        &self,
251
        event: Event,
252
        _write: &MutexGuard<'_, ()>,
253
    ) -> Result<SaveEventStatus> {
254
        self.run(move |connection| save_event(connection, &event))
255
            .await
256
    }
257
258
    pub async fn status(&self, event_id: EventId) -> Result<DatabaseEventStatus> {
259
        self.run(move |connection| event_status(connection, &event_id))
260
            .await
261
    }
262
263
    pub async fn event(&self, event_id: EventId) -> Result<Option<Event>> {
264
        self.run(move |connection| event_by_id(connection, &event_id))
265
            .await
266
    }
267
268
    pub async fn count(&self, filter: Filter) -> Result<usize> {
269
        self.run(move |connection| Ok(query_events(connection, &filter, usize::MAX)?.len()))
270
            .await
271
    }
272
273
    pub async fn query(&self, filter: Filter, max_bytes: usize) -> Result<BTreeSet<Event>> {
274
        self.run(move |connection| query_events(connection, &filter, max_bytes))
275
            .await
276
    }
277
278
    pub async fn replace_discovered_locked(
279
        &self,
280
        signer: PublicKey,
281
        urls: Vec<String>,
282
        max: usize,
283
        _write: &MutexGuard<'_, ()>,
284
    ) -> Result<()> {
285
        let signer_bytes = signer.as_bytes().to_vec();
286
        self.run(move |connection| replace_discovered(connection, &signer_bytes, urls, max))
287
            .await
288
    }
289
290
    pub async fn discovered(&self, limit: usize) -> Result<Vec<String>> {
291
        self.run(move |connection| list_discovered(connection, limit))
292
            .await
293
    }
294
295
    pub async fn forget_discovered(&self, urls: Vec<String>) -> Result<()> {
296
        self.write(move |connection| forget_discovered(connection, urls))
297
            .await
298
    }
299
300
    async fn write<T, F>(&self, operation: F) -> Result<T>
301
    where
302
        T: Send + 'static,
303
        F: FnOnce(&mut Connection) -> Result<T> + Send + 'static,
304
    {
305
        let _write = self.writes.lock().await;
306
        self.run(operation).await
307
    }
308
309
    async fn run<T, F>(&self, operation: F) -> Result<T>
310
    where
311
        T: Send + 'static,
312
        F: FnOnce(&mut Connection) -> Result<T> + Send + 'static,
313
    {
314
        let shared_connection = Arc::clone(&self.connection);
315
        let mut connection = shared_connection.lock_owned().await;
316
        spawn_blocking(move || operation(&mut connection))
317
            .await
318
            .context("journal task panicked")?
319
    }
320
}
321
322
fn open_connection(
323
    path: &Path,
324
    max_database_bytes: u64,
325
    max_discovered_peers: usize,
326
    excluded_peers: &[String],
327
) -> Result<Connection> {
328
    let mut connection =
329
        Connection::open(path).with_context(|| format!("opening journal {}", path.display()))?;
330
    configure(&connection, max_database_bytes)?;
331
    migrate(&mut connection, max_discovered_peers, excluded_peers)?;
332
    Ok(connection)
333
}
334
335
fn pending_deliveries(connection: &Connection, peer: &str, limit: usize) -> Result<Vec<Delivery>> {
336
    if limit == 0 {
337
        return Ok(Vec::new());
338
    }
339
340
    let retry_limit = limit / 2;
341
    let mut pending = if retry_limit == 0 {
342
        Vec::new()
343
    } else {
344
        read_deliveries(
345
            connection,
346
            DUE_DELIVERIES,
347
            params![peer, unix_seconds(), integer(retry_limit)?],
348
        )?
349
    };
350
351
    let remaining = limit - pending.len();
352
    if remaining == 0 {
353
        return Ok(pending);
354
    }
355
356
    let delivered = read_delivered(connection, peer)?;
357
    pending.extend(read_deliveries(
358
        connection,
359
        NEW_DELIVERIES,
360
        params![delivered, integer(remaining)?],
361
    )?);
362
    Ok(pending)
363
}
364
365
fn read_deliveries<P>(connection: &Connection, sql: &str, parameters: P) -> Result<Vec<Delivery>>
366
where
367
    P: Params,
368
{
369
    let mut statement = connection.prepare(sql)?;
370
    let mut rows = statement.query(parameters)?;
371
    let mut deliveries = Vec::new();
372
373
    while let Some(row) = rows.next()? {
374
        let sequence = row.get::<_, i64>(0)?;
375
        let event_id = row.get::<_, Vec<u8>>(1)?;
376
        deliveries.push((
377
            u64::try_from(sequence).context("negative delivery sequence")?,
378
            EventId::from_slice(&event_id)
379
                .map_err(|error| anyhow::anyhow!("invalid event id {error}"))?,
380
        ));
381
    }
382
383
    Ok(deliveries)
384
}
385
386
fn read_delivered(connection: &Connection, peer: &str) -> Result<i64> {
387
    Ok(connection
388
        .query_row(
389
            "SELECT delivered FROM vineyard_peers WHERE peer = ?1",
390
            [peer],
391
            |row| row.get(0),
392
        )
393
        .optional()?
394
        .unwrap_or(0))
395
}
396
397
fn acknowledge_delivery(connection: &mut Connection, peer: &str, sequence: u64) -> Result<()> {
398
    let stored_sequence = integer(sequence)?;
399
    let transaction = connection.transaction()?;
400
    transaction.execute(
401
        "DELETE FROM vineyard_retries WHERE peer = ?1 AND sequence = ?2",
402
        params![peer, stored_sequence],
403
    )?;
404
    advance_delivery(&transaction, peer, stored_sequence)?;
405
    transaction.commit()?;
406
    Ok(())
407
}
408
409
fn defer_delivery(
410
    connection: &mut Connection,
411
    peer: &str,
412
    sequence: u64,
413
    error: &str,
414
    retry_at: u64,
415
) -> Result<()> {
416
    let stored_sequence = integer(sequence)?;
417
    let stored_retry_at = integer(retry_at)?;
418
    let transaction = connection.transaction()?;
419
    advance_delivery(&transaction, peer, stored_sequence)?;
420
    transaction.execute(
421
        "
422
            INSERT INTO vineyard_retries(peer, sequence, retry_at, error)
423
            VALUES (?1, ?2, ?3, ?4)
424
            ON CONFLICT(peer, sequence) DO UPDATE SET
425
                retry_at = excluded.retry_at,
426
                error = excluded.error
427
            ",
428
        params![peer, stored_sequence, stored_retry_at, error],
429
    )?;
430
    transaction.commit()?;
431
    Ok(())
432
}
433
434
fn read_checkpoint(connection: &Connection, peer: &str, scope: &str) -> Result<Option<u64>> {
435
    let checkpoint = connection
436
        .query_row(
437
            "SELECT until FROM vineyard_checkpoints WHERE peer = ?1 AND scope = ?2",
438
            params![peer, scope],
439
            |row| row.get::<_, i64>(0),
440
        )
441
        .optional()?;
442
443
    checkpoint
444
        .map(|value| u64::try_from(value).context("negative checkpoint"))
445
        .transpose()
446
}
447
448
fn write_checkpoint(connection: &Connection, peer: &str, scope: &str, until: u64) -> Result<()> {
449
    connection.execute(
450
        "
451
            INSERT INTO vineyard_checkpoints(peer, scope, until)
452
            VALUES (?1, ?2, ?3)
453
            ON CONFLICT(peer, scope) DO UPDATE SET
454
                until = max(vineyard_checkpoints.until, excluded.until)
455
            ",
456
        params![peer, scope, integer(until)?],
457
    )?;
458
    Ok(())
459
}
460
461
fn list_repositories(connection: &Connection, limit: usize) -> Result<Vec<(String, String)>> {
462
    let mut statement = connection.prepare(REPOSITORIES)?;
463
    let mut rows = statement.query([])?;
464
    let mut repositories = Vec::new();
465
    let mut seen = BTreeSet::new();
466
    while let Some(row) = rows.next()? {
467
        let tags = serde_json::from_str::<Tags>(&row.get::<_, String>(2)?)?;
468
        if tags
469
            .expiration()
470
            .is_some_and(|expiration| expiration < Timestamp::now())
471
        {
472
            continue;
473
        }
474
        let repository = (row.get(0)?, row.get(1)?);
475
        if !seen.insert(repository.clone()) {
476
            continue;
477
        }
478
        repositories.push(repository);
479
        if repositories.len() == limit {
480
            break;
481
        }
482
    }
483
    Ok(repositories)
484
}
485
486
fn event_status(connection: &Connection, event_id: &EventId) -> Result<DatabaseEventStatus> {
487
    let deleted = connection.query_row(
488
        "SELECT EXISTS(SELECT 1 FROM deleted_ids WHERE event_id = ?1)",
489
        [event_id.as_bytes().as_slice()],
490
        |row| row.get::<_, bool>(0),
491
    )?;
492
    if deleted {
493
        return Ok(DatabaseEventStatus::Deleted);
494
    }
495
    let saved = connection.query_row(
496
        "SELECT EXISTS(SELECT 1 FROM events WHERE id = ?1)",
497
        [event_id.as_bytes().as_slice()],
498
        |row| row.get::<_, bool>(0),
499
    )?;
500
    Ok(if saved {
501
        DatabaseEventStatus::Saved
502
    } else {
503
        DatabaseEventStatus::NotExistent
504
    })
505
}
506
507
fn event_by_id(connection: &Connection, event_id: &EventId) -> Result<Option<Event>> {
508
    connection
509
        .query_row(
510
            "
511
            SELECT id, pubkey, created_at, kind, content, tags, sig
512
            FROM events
513
            WHERE id = ?1
514
            ",
515
            [event_id.as_bytes().as_slice()],
516
            StoredEvent::read,
517
        )
518
        .optional()?
519
        .map(StoredEvent::into_event)
520
        .transpose()
521
}
522
523
fn save_event(connection: &mut Connection, event: &Event) -> Result<SaveEventStatus> {
524
    if event.kind.is_ephemeral() {
525
        return Ok(SaveEventStatus::Rejected(RejectedReason::Ephemeral));
526
    }
527
528
    let transaction = connection.transaction()?;
529
    if event_exists(&transaction, &event.id)? {
530
        return Ok(SaveEventStatus::Rejected(RejectedReason::Duplicate));
531
    }
532
    if event_deleted(&transaction, &event.id)? || coordinate_deleted(&transaction, event)? {
533
        return Ok(SaveEventStatus::Rejected(RejectedReason::Deleted));
534
    }
535
    if replacement_rejected(&transaction, event)? {
536
        return Ok(SaveEventStatus::Rejected(RejectedReason::Replaced));
537
    }
538
    if event.kind == Kind::EventDeletion && !apply_deletion(&transaction, event)? {
539
        return Ok(SaveEventStatus::Rejected(RejectedReason::InvalidDelete));
540
    }
541
542
    transaction.execute(
543
        "
544
        INSERT INTO events(id, pubkey, created_at, kind, content, tags, sig)
545
        VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
546
        ",
547
        params![
548
            event.id.as_bytes().as_slice(),
549
            event.pubkey.as_bytes().as_slice(),
550
            integer(event.created_at.as_secs())?,
551
            i64::from(event.kind.as_u16()),
552
            &event.content,
553
            serde_json::to_string(&event.tags)?,
554
            event.sig.as_bytes().as_slice(),
555
        ],
556
    )?;
557
    for tag in event.tags.iter() {
558
        let (Some(name), Some(value)) = (tag.single_letter_tag(), tag.content()) else {
559
            continue;
560
        };
561
        transaction.execute(
562
            "
563
            INSERT OR IGNORE INTO event_tags(event_id, tag_name, tag_value)
564
            VALUES (?1, ?2, ?3)
565
            ",
566
            params![event.id.as_bytes().as_slice(), name.to_string(), value],
567
        )?;
568
    }
569
    transaction.commit()?;
570
    Ok(SaveEventStatus::Success)
571
}
572
573
fn event_exists(transaction: &Transaction<'_>, event_id: &EventId) -> Result<bool> {
574
    Ok(transaction.query_row(
575
        "SELECT EXISTS(SELECT 1 FROM events WHERE id = ?1)",
576
        [event_id.as_bytes().as_slice()],
577
        |row| row.get(0),
578
    )?)
579
}
580
581
fn event_deleted(transaction: &Transaction<'_>, event_id: &EventId) -> Result<bool> {
582
    Ok(transaction.query_row(
583
        "SELECT EXISTS(SELECT 1 FROM deleted_ids WHERE event_id = ?1)",
584
        [event_id.as_bytes().as_slice()],
585
        |row| row.get(0),
586
    )?)
587
}
588
589
fn coordinate_deleted(transaction: &Transaction<'_>, event: &Event) -> Result<bool> {
590
    let Some(coordinate) = event.coordinate() else {
591
        return Ok(false);
592
    };
593
    let deleted_at = transaction
594
        .query_row(
595
            "
596
            SELECT deleted_at
597
            FROM deleted_coordinates
598
            WHERE pubkey = ?1 AND kind = ?2 AND identifier = ?3
599
            ",
600
            params![
601
                coordinate.public_key.as_bytes().as_slice(),
602
                i64::from(coordinate.kind.as_u16()),
603
                coordinate.identifier,
604
            ],
605
            |row| row.get::<_, i64>(0),
606
        )
607
        .optional()?;
608
    Ok(deleted_at.is_some_and(|stored_deleted_at| {
609
        u64::try_from(stored_deleted_at).map_or(true, |value| event.created_at.as_secs() <= value)
610
    }))
611
}
612
613
fn replacement_rejected(transaction: &Transaction<'_>, event: &Event) -> Result<bool> {
614
    let existing = if event.kind.is_replaceable() {
615
        transaction
616
            .query_row(
617
                "
618
                SELECT id, created_at
619
                FROM events
620
                WHERE pubkey = ?1 AND kind = ?2
621
                LIMIT 1
622
                ",
623
                params![
624
                    event.pubkey.as_bytes().as_slice(),
625
                    i64::from(event.kind.as_u16()),
626
                ],
627
                |row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, i64>(1)?)),
628
            )
629
            .optional()?
630
    } else if event.kind.is_addressable() {
631
        let Some(identifier) = event.tags.identifier() else {
632
            return Ok(false);
633
        };
634
        transaction
635
            .query_row(
636
                "
637
                SELECT event.id, event.created_at
638
                FROM events AS event
639
                JOIN event_tags AS tag ON tag.event_id = event.id
640
                WHERE event.pubkey = ?1
641
                  AND event.kind = ?2
642
                  AND tag.tag_name = 'd'
643
                  AND tag.tag_value = ?3
644
                LIMIT 1
645
                ",
646
                params![
647
                    event.pubkey.as_bytes().as_slice(),
648
                    i64::from(event.kind.as_u16()),
649
                    identifier,
650
                ],
651
                |row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, i64>(1)?)),
652
            )
653
            .optional()?
654
    } else {
655
        return Ok(false);
656
    };
657
658
    let Some((stored_id, stored_at)) = existing else {
659
        return Ok(false);
660
    };
661
    let incoming_at = integer(event.created_at.as_secs())?;
662
    match stored_at.cmp(&incoming_at) {
663
        Ordering::Greater => Ok(true),
664
        Ordering::Equal if stored_id.as_slice() < event.id.as_bytes().as_slice() => Ok(true),
665
        Ordering::Equal | Ordering::Less => {
666
            transaction.execute("DELETE FROM events WHERE id = ?1", [stored_id])?;
667
            Ok(false)
668
        }
669
    }
670
}
671
672
fn apply_deletion(transaction: &Transaction<'_>, event: &Event) -> Result<bool> {
673
    for event_id in event.tags.event_ids() {
674
        let owner = transaction
675
            .query_row(
676
                "SELECT pubkey FROM events WHERE id = ?1",
677
                [event_id.as_bytes().as_slice()],
678
                |row| row.get::<_, Vec<u8>>(0),
679
            )
680
            .optional()?;
681
        if owner.is_some_and(|saved_owner| saved_owner.as_slice() != event.pubkey.as_bytes()) {
682
            return Ok(false);
683
        }
684
        transaction.execute(
685
            "INSERT OR IGNORE INTO deleted_ids(event_id) VALUES (?1)",
686
            [event_id.as_bytes().as_slice()],
687
        )?;
688
        transaction.execute(
689
            "DELETE FROM events WHERE id = ?1 AND pubkey = ?2",
690
            params![
691
                event_id.as_bytes().as_slice(),
692
                event.pubkey.as_bytes().as_slice(),
693
            ],
694
        )?;
695
    }
696
697
    for coordinate in event.tags.coordinates() {
698
        if coordinate.public_key != event.pubkey {
699
            return Ok(false);
700
        }
701
        transaction.execute(
702
            "
703
            INSERT INTO deleted_coordinates(pubkey, kind, identifier, deleted_at)
704
            VALUES (?1, ?2, ?3, ?4)
705
            ON CONFLICT(pubkey, kind, identifier) DO UPDATE SET
706
                deleted_at = max(deleted_coordinates.deleted_at, excluded.deleted_at)
707
            ",
708
            params![
709
                coordinate.public_key.as_bytes().as_slice(),
710
                i64::from(coordinate.kind.as_u16()),
711
                &coordinate.identifier,
712
                integer(event.created_at.as_secs())?,
713
            ],
714
        )?;
715
        if coordinate.kind.is_replaceable() {
716
            transaction.execute(
717
                "
718
                DELETE FROM events
719
                WHERE pubkey = ?1 AND kind = ?2 AND created_at <= ?3
720
                ",
721
                params![
722
                    coordinate.public_key.as_bytes().as_slice(),
723
                    i64::from(coordinate.kind.as_u16()),
724
                    integer(event.created_at.as_secs())?,
725
                ],
726
            )?;
727
        } else {
728
            transaction.execute(
729
                "
730
                DELETE FROM events
731
                WHERE id IN (
732
                    SELECT stored.id
733
                    FROM events AS stored
734
                    JOIN event_tags AS tag ON tag.event_id = stored.id
735
                    WHERE stored.pubkey = ?1
736
                      AND stored.kind = ?2
737
                      AND stored.created_at <= ?3
738
                      AND tag.tag_name = 'd'
739
                      AND tag.tag_value = ?4
740
                )
741
                ",
742
                params![
743
                    coordinate.public_key.as_bytes().as_slice(),
744
                    i64::from(coordinate.kind.as_u16()),
745
                    integer(event.created_at.as_secs())?,
746
                    &coordinate.identifier,
747
                ],
748
            )?;
749
        }
750
    }
751
    Ok(true)
752
}
753
754
fn query_events(
755
    connection: &Connection,
756
    filter: &Filter,
757
    max_bytes: usize,
758
) -> Result<BTreeSet<Event>> {
759
    let query = EventQuery::new(filter)?;
760
    if query.limit == 0 {
761
        return Ok(BTreeSet::new());
762
    }
763
    let mut statement = connection.prepare(&query.sql)?;
764
    let mut rows = statement.query(params_from_iter(query.parameters))?;
765
    let mut events = BTreeSet::new();
766
    let mut bytes = 0_usize;
767
768
    while let Some(row) = rows.next()? {
769
        let event = StoredEvent::read(row)?.into_event()?;
770
        if event.is_expired() {
771
            continue;
772
        }
773
        bytes = bytes
774
            .checked_add(event.as_json().len())
775
            .context("history byte count overflow")?;
776
        if bytes > max_bytes {
777
            anyhow::bail!("history byte limit exceeded");
778
        }
779
        events.insert(event);
780
        if events.len() == query.limit {
781
            break;
782
        }
783
    }
784
785
    Ok(events)
786
}
787
788
fn replace_discovered(
789
    connection: &mut Connection,
790
    signer: &[u8],
791
    urls: Vec<String>,
792
    max: usize,
793
) -> Result<()> {
794
    let transaction = connection.transaction()?;
795
    transaction.execute(
796
        "DELETE FROM vineyard_discovered_peers WHERE signer = ?1",
797
        [signer],
798
    )?;
799
    let stored_count = transaction.query_row(
800
        "SELECT count(DISTINCT url) FROM vineyard_discovered_peers",
801
        [],
802
        |row| row.get::<_, i64>(0),
803
    )?;
804
    let count = usize::try_from(stored_count).context("negative discovered peer count")?;
805
806
    for url in urls.into_iter().take(max.saturating_sub(count)) {
807
        transaction.execute(
808
            "
809
                INSERT OR IGNORE INTO vineyard_discovered_peers(signer, url, discovered_at)
810
                VALUES (?1, ?2, unixepoch())
811
                ",
812
            params![signer, url],
813
        )?;
814
    }
815
816
    transaction.commit()?;
817
    Ok(())
818
}
819
820
fn list_discovered(connection: &Connection, limit: usize) -> Result<Vec<String>> {
821
    let mut statement = connection
822
        .prepare("SELECT DISTINCT url FROM vineyard_discovered_peers ORDER BY url LIMIT ?1")?;
823
    let mut rows = statement.query([integer(limit)?])?;
824
    let mut urls = Vec::new();
825
    while let Some(row) = rows.next()? {
826
        urls.push(row.get(0)?);
827
    }
828
    Ok(urls)
829
}
830
831
fn forget_discovered(connection: &mut Connection, urls: Vec<String>) -> Result<()> {
832
    let transaction = connection.transaction()?;
833
    for url in urls {
834
        transaction.execute(
835
            "DELETE FROM vineyard_discovered_peers WHERE url = ?1",
836
            [url],
837
        )?;
838
    }
839
    transaction.commit()?;
840
    Ok(())
841
}
842
843
impl StoredEvent {
844
    fn read(row: &Row<'_>) -> rusqlite::Result<Self> {
845
        Ok(Self {
846
            id: row.get(0)?,
847
            pubkey: row.get(1)?,
848
            created_at: row.get(2)?,
849
            kind: row.get(3)?,
850
            content: row.get(4)?,
851
            tags: row.get(5)?,
852
            signature: row.get(6)?,
853
        })
854
    }
855
856
    fn into_event(self) -> Result<Event> {
857
        let id = EventId::from_slice(&self.id).context("invalid stored event id")?;
858
        let pubkey = PublicKey::from_slice(&self.pubkey).context("invalid stored public key")?;
859
        let created_at: Timestamp = self
860
            .created_at
861
            .try_into()
862
            .context("invalid event timestamp")?;
863
        let kind = Kind::from_u16(self.kind.try_into().context("invalid stored event kind")?);
864
        let tags: Tags = serde_json::from_str(&self.tags).context("invalid stored event tags")?;
865
        let signature =
866
            Signature::from_slice(&self.signature).context("invalid stored event signature")?;
867
        Ok(Event::new(
868
            id,
869
            pubkey,
870
            created_at,
871
            kind,
872
            tags,
873
            self.content,
874
            signature,
875
        ))
876
    }
877
}
878
879
fn configure(connection: &Connection, max_database_bytes: u64) -> Result<()> {
880
    connection.busy_timeout(Duration::from_secs(5))?;
881
    connection.pragma_update(None, "journal_mode", "WAL")?;
882
    let journal_mode: String =
883
        connection.pragma_query_value(None, "journal_mode", |row| row.get(0))?;
884
    if !journal_mode.eq_ignore_ascii_case("wal") {
885
        anyhow::bail!("journal mode is not wal");
886
    }
887
888
    connection.pragma_update(None, "synchronous", "FULL")?;
889
    connection.pragma_update(None, "foreign_keys", "ON")?;
890
891
    let stored_page_size =
892
        connection.query_row("PRAGMA page_size", [], |row| row.get::<_, i64>(0))?;
893
    let page_size = u64::try_from(stored_page_size).context("invalid database page size")?;
894
    let page_budget = max_database_bytes / page_size;
895
    if page_budget == 0 {
896
        anyhow::bail!("database budget is smaller than one page");
897
    }
898
    let max_pages = integer(page_budget)?;
899
    let page_count = connection.query_row("PRAGMA page_count", [], |row| row.get::<_, i64>(0))?;
900
    if page_count > max_pages {
901
        anyhow::bail!("database exceeds size budget");
902
    }
903
    connection.pragma_update(None, "max_page_count", max_pages)?;
904
    Ok(())
905
}
906
907
fn migrate(
908
    connection: &mut Connection,
909
    max_discovered_peers: usize,
910
    excluded_peers: &[String],
911
) -> Result<()> {
912
    let transaction = connection.transaction()?;
913
    migrate_legacy_tables(&transaction)?;
914
    transaction.execute_batch(SCHEMA)?;
915
    transaction.execute(BACKFILL_DELIVERY, [])?;
916
    transaction.commit()?;
917
    backfill_discovered(connection, max_discovered_peers, excluded_peers)?;
918
    Ok(())
919
}
920
921
fn migrate_legacy_tables(transaction: &Transaction<'_>) -> Result<()> {
922
    for (old, new) in [
923
        ("sanctum_delivery", "vineyard_delivery"),
924
        ("sanctum_peers", "vineyard_peers"),
925
        ("sanctum_checkpoints", "vineyard_checkpoints"),
926
        ("sanctum_retries", "vineyard_retries"),
927
        ("sanctum_discovered_peers", "vineyard_discovered_peers"),
928
    ] {
929
        let old_exists = table_exists(transaction, old)?;
930
        let new_exists = table_exists(transaction, new)?;
931
        if old_exists && new_exists {
932
            anyhow::bail!("database contains both {old} and {new}");
933
        }
934
        if old_exists {
935
            transaction.execute_batch(&format!("ALTER TABLE {old} RENAME TO {new}"))?;
936
        }
937
    }
938
    transaction.execute_batch(
939
        "
940
        DROP TRIGGER IF EXISTS sanctum_delivery_after_insert;
941
        DROP INDEX IF EXISTS sanctum_retries_due;
942
        DROP INDEX IF EXISTS sanctum_discovered_urls;
943
        ",
944
    )?;
945
    Ok(())
946
}
947
948
fn table_exists(transaction: &Transaction<'_>, name: &str) -> Result<bool> {
949
    Ok(transaction.query_row(
950
        "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1)",
951
        [name],
952
        |row| row.get(0),
953
    )?)
954
}
955
956
fn backfill_discovered(
957
    connection: &mut Connection,
958
    max_discovered_peers: usize,
959
    excluded_peers: &[String],
960
) -> Result<()> {
961
    let excluded = excluded_peers.iter().cloned().collect::<HashSet<_>>();
962
    let relay_tags = {
963
        let mut statement = connection.prepare(DISCOVERED_RELAY_TAGS)?;
964
        let mut rows = statement.query([])?;
965
        let mut relay_tags = Vec::new();
966
        while let Some(row) = rows.next()? {
967
            relay_tags.push((
968
                row.get::<_, Vec<u8>>(0)?,
969
                row.get::<_, String>(1)?,
970
                row.get::<_, i64>(2)?,
971
            ));
972
        }
973
        relay_tags
974
    };
975
976
    let transaction = connection.transaction()?;
977
    transaction.execute("DELETE FROM vineyard_discovered_peers", [])?;
978
    let mut urls = HashSet::new();
979
    for (signer, raw_url, discovered_at) in relay_tags {
980
        let Some(url) = relay_url(&raw_url).map(|url| url.to_string()) else {
981
            continue;
982
        };
983
        if excluded.contains(&url) || (!urls.contains(&url) && urls.len() >= max_discovered_peers) {
984
            continue;
985
        }
986
        urls.insert(url.clone());
987
        transaction.execute(
988
            "
989
            INSERT OR IGNORE INTO vineyard_discovered_peers(signer, url, discovered_at)
990
            VALUES (?1, ?2, ?3)
991
            ",
992
            params![signer, url, discovered_at],
993
        )?;
994
    }
995
    transaction.commit()?;
996
    Ok(())
997
}
998
999
fn advance_delivery(transaction: &Transaction<'_>, peer: &str, sequence: i64) -> Result<()> {
1000
    transaction.execute(
1001
        "
1002
        INSERT INTO vineyard_peers(peer, delivered)
1003
        VALUES (?1, ?2)
1004
        ON CONFLICT(peer) DO UPDATE SET
1005
            delivered = max(vineyard_peers.delivered, excluded.delivered)
1006
        ",
1007
        params![peer, sequence],
1008
    )?;
1009
    Ok(())
1010
}
1011
1012
fn integer<T>(value: T) -> Result<i64>
1013
where
1014
    T: TryInto<i64>,
1015
    T::Error: Error + Send + Sync + 'static,
1016
{
1017
    value
1018
        .try_into()
1019
        .context("value exceeds sqlite integer range")
1020
}
1021
1022
fn unix_seconds() -> i64 {
1023
    SystemTime::now()
1024
        .duration_since(UNIX_EPOCH)
1025
        .map_or(0, |duration| {
1026
            i64::try_from(duration.as_secs()).unwrap_or(i64::MAX)
1027
        })
1028
}