| 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 | } |