From 2f8ecd482077d82f2d1a937c7f979eaaa87a27b2 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 3 Dec 2025 08:54:00 +0000 Subject: feat: implement LMDB database backend - Add nostr-lmdb dependency (v0.44) for persistent storage - Create SharedDatabase type alias for database abstraction - Update all database-related functions to use trait object - Support runtime selection via NGIT_DATABASE_BACKEND env var Database backends: - memory: In-memory (default, fastest, no persistence) - lmdb: LMDB backend (persistent, general purpose) All 34 tests pass with the new implementation. --- src/git/authorization.rs | 12 +++++------ src/git/handlers.rs | 7 +++--- src/http/mod.rs | 9 ++++---- src/nostr/builder.rs | 56 ++++++++++++++++++++++++++++++------------------ 4 files changed, 48 insertions(+), 36 deletions(-) (limited to 'src') diff --git a/src/git/authorization.rs b/src/git/authorization.rs index 3b0e759..4896fc0 100644 --- a/src/git/authorization.rs +++ b/src/git/authorization.rs @@ -31,9 +31,9 @@ use anyhow::{anyhow, Result}; use nostr_relay_builder::prelude::*; use nostr_sdk::{EventId, ToBech32}; use std::collections::{HashMap, HashSet}; -use std::sync::Arc; use tracing::debug; +use crate::nostr::builder::SharedDatabase; use crate::nostr::events::{ RepositoryAnnouncement, RepositoryState, KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, @@ -56,7 +56,7 @@ pub struct RepositoryData { /// This performs a single database query to fetch both announcement and state events, /// which is more efficient than separate queries. pub async fn fetch_repository_data( - database: &Arc, + database: &SharedDatabase, identifier: &str, ) -> Result { let filter = Filter::new() @@ -284,7 +284,7 @@ pub fn is_latest_state( /// /// Returns an `AuthorizationResult` that indicates whether a push is authorized. pub async fn get_authorization_from_db( - database: &Arc, + database: &SharedDatabase, identifier: &str, ) -> Result { // Fetch all repository data with a single query @@ -340,7 +340,7 @@ pub async fn get_authorization_from_db( /// /// Returns an `AuthorizationResult` that indicates whether a push is authorized. pub async fn get_authorization_for_owner( - database: &Arc, + database: &SharedDatabase, identifier: &str, owner_pubkey: &str, ) -> Result { @@ -817,7 +817,7 @@ pub fn npub_to_pubkey(npub: &str) -> Result { /// - `Ok(None)` if the event doesn't exist (push should be allowed) /// - `Err(_)` on database errors pub async fn get_event_commit_tag( - database: &Arc, + database: &SharedDatabase, event_id: &EventId, ) -> Result> { // Query for PR (1618) and PR Update (1619) events with this ID @@ -872,7 +872,7 @@ pub async fn get_event_commit_tag( /// * `Ok(())` if all refs/nostr/ pushes are valid /// * `Err(_)` if any ref has invalid event ID format or fails commit validation pub async fn validate_nostr_ref_pushes( - database: &Arc, + database: &SharedDatabase, pushed_refs: &[(String, String, String)], ) -> Result<()> { for (_, new_oid, ref_name) in pushed_refs { diff --git a/src/git/handlers.rs b/src/git/handlers.rs index e84cabb..8e5f5e1 100644 --- a/src/git/handlers.rs +++ b/src/git/handlers.rs @@ -4,9 +4,7 @@ use http_body_util::Full; use hyper::{body::Bytes, Response, StatusCode}; -use nostr_relay_builder::prelude::MemoryDatabase; use std::path::PathBuf; -use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tracing::{debug, error, info, warn}; @@ -18,6 +16,7 @@ use super::protocol::{GitService, PktLine}; use super::subprocess::GitSubprocess; use super::try_set_head_if_available; +use crate::nostr::builder::SharedDatabase; use crate::nostr::events::RepositoryState; /// Handle GET /info/refs?service=git-{upload,receive}-pack @@ -178,7 +177,7 @@ pub async fn handle_upload_pack( pub async fn handle_receive_pack( repo_path: PathBuf, request_body: Bytes, - database: Option>, + database: Option, identifier: &str, owner_pubkey: &str, ) -> Result>, GitError> { @@ -310,7 +309,7 @@ pub async fn handle_receive_pack( /// 5. Validates that pushed refs match the state /// 6. Validates refs/nostr/ has valid event id and if event exists, `c` tag matches ref async fn authorize_push( - database: &Arc, + database: &SharedDatabase, identifier: &str, owner_pubkey: &str, request_body: &Bytes, diff --git a/src/http/mod.rs b/src/http/mod.rs index 5cf8dbe..4665281 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -7,7 +7,6 @@ pub mod nip11; use std::future::Future; use std::net::SocketAddr; use std::pin::Pin; -use std::sync::Arc; use base64::Engine; use http_body_util::{BodyExt, Full}; @@ -17,7 +16,6 @@ use hyper::server::conn::http1; use hyper::service::Service; use hyper::{Method, Request, Response}; use hyper_util::rt::TokioIo; -use nostr_relay_builder::prelude::MemoryDatabase; use nostr_relay_builder::LocalRelay; use nostr_sdk::hashes::sha1::Hash as Sha1Hash; use nostr_sdk::hashes::{Hash, HashEngine}; @@ -26,6 +24,7 @@ use tokio::net::TcpListener; use crate::config::Config; use crate::git; +use crate::nostr::builder::SharedDatabase; /// CORS headers required by GRASP-01 specification (lines 40-47) const CORS_ALLOW_ORIGIN: &str = "*"; @@ -90,7 +89,7 @@ struct HttpService { config: Config, remote: SocketAddr, /// Database reference for direct queries (e.g., push authorization) - database: Arc, + database: SharedDatabase, } impl HttpService { @@ -98,7 +97,7 @@ impl HttpService { relay: LocalRelay, config: Config, remote: SocketAddr, - database: Arc, + database: SharedDatabase, ) -> Self { Self { relay, @@ -423,7 +422,7 @@ fn derive_accept_key(request_key: &[u8]) -> String { pub async fn run_server( config: Config, relay: LocalRelay, - database: Arc, + database: SharedDatabase, ) -> anyhow::Result<()> { let bind_addr: SocketAddr = config.bind_address.parse()?; diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index 2f182ea..eabb38f 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use nostr::nips::nip19::ToBech32; use nostr::prelude::{Alphabet, SingleLetterTag}; use nostr::{EventId, Filter, Kind, PublicKey}; +use nostr_lmdb::NostrLMDB; use nostr_relay_builder::prelude::*; use crate::config::{Config, DatabaseBackend}; @@ -18,6 +19,9 @@ use crate::nostr::events::{ KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, }; +/// Type alias for the shared database used by the relay +pub type SharedDatabase = Arc; + /// Result of aligning a repository with authorized state #[derive(Debug, Default)] struct AlignmentResult { @@ -35,23 +39,33 @@ struct AlignmentResult { /// /// Validates all events according to GRASP-01 specification: /// - Repository announcements must list service in clone and relays tags -/// - Repository state announcements must have valid structure +/// - Repository state announcements must have valid structure /// - Other events must reference accepted repositories or events /// - Forward references are supported (events referenced by accepted events) /// - Orphan events with no valid references are rejected /// /// Uses stateful database queries to check event relationships. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct Nip34WritePolicy { domain: String, - database: Arc, + database: SharedDatabase, git_data_path: PathBuf, } +impl std::fmt::Debug for Nip34WritePolicy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Nip34WritePolicy") + .field("domain", &self.domain) + .field("git_data_path", &self.git_data_path) + .field("database", &"") + .finish() + } +} + impl Nip34WritePolicy { pub fn new( domain: impl Into, - database: Arc, + database: SharedDatabase, git_data_path: impl Into, ) -> Self { Self { @@ -104,7 +118,7 @@ impl Nip34WritePolicy { /// The authorized_pubkeys should be the owner and maintainers of a specific /// announcement, so different owners with the same identifier don't interfere. async fn is_latest_state_for_identifier( - database: &Arc, + database: &SharedDatabase, state: &RepositoryState, authorized_pubkeys: &[PublicKey], ) -> Result { @@ -155,7 +169,7 @@ impl Nip34WritePolicy { /// should update HEAD in the repository of the announcement owner, /// not in the maintainer's own (possibly non-existent) repository. async fn find_authorized_announcements( - database: &Arc, + database: &SharedDatabase, identifier: &str, state_author: &PublicKey, ) -> Result, String> { @@ -205,7 +219,7 @@ impl Nip34WritePolicy { /// - This state event is the latest for the identifier in that context async fn identify_owner_repositories( &self, - database: &Arc, + database: &SharedDatabase, state: &RepositoryState, ) -> Result, String> { // Find all announcements where state author is authorized @@ -485,7 +499,7 @@ impl Nip34WritePolicy { /// Ok(Some(n)) if n refs were deleted, Ok(None) if no action taken, Err on failure async fn validate_pr_nostr_ref( &self, - database: &Arc, + database: &SharedDatabase, event: &Event, ) -> Result, String> { let event_id = event.id.to_hex(); @@ -651,7 +665,7 @@ impl Nip34WritePolicy { /// Check if any addressable events (repositories) exist in database /// Returns the first matching addressable reference found, or None if none match async fn find_accepted_repository( - database: &Arc, + database: &SharedDatabase, addressables: &[String], ) -> Result, String> { if addressables.is_empty() { @@ -724,7 +738,7 @@ impl Nip34WritePolicy { /// Check if any events exist in database /// Returns the first matching event ID found, or None if none match async fn find_accepted_event( - database: &Arc, + database: &SharedDatabase, event_ids: &[EventId], ) -> Result, String> { if event_ids.is_empty() { @@ -752,7 +766,7 @@ impl Nip34WritePolicy { /// This optimization recognizes that replaceable events are referenced by coordinate address, /// while regular events are referenced by event ID. async fn is_referenced_by_accepted( - database: &Arc, + database: &SharedDatabase, event: &Event, ) -> Result { let kind_u16 = event.kind.as_u16(); @@ -1152,14 +1166,14 @@ pub struct RelayWithDatabase { /// The local relay instance pub relay: LocalRelay, /// The database Arc that can be used for direct queries - pub database: Arc, + pub database: SharedDatabase, } /// Create a configured LocalRelay with full GRASP-01 validation /// /// Returns a `RelayWithDatabase` struct containing: /// - The `LocalRelay` for handling WebSocket connections -/// - The `Arc` for direct database queries (e.g., push authorization) +/// - The `SharedDatabase` for direct database queries (e.g., push authorization) pub fn create_relay(config: &Config) -> Result { tracing::info!("Configuring nostr relay with GRASP-01 validation..."); @@ -1167,7 +1181,7 @@ pub fn create_relay(config: &Config) -> Result { let db_path = Path::new(&config.relay_data_path); // Create database based on configuration - let database = match config.database_backend { + let database: SharedDatabase = match config.database_backend { DatabaseBackend::Memory => { tracing::info!("Using in-memory database (no persistence)"); Arc::new(MemoryDatabase::with_opts(MemoryDatabaseOptions { @@ -1187,13 +1201,13 @@ pub fn create_relay(config: &Config) -> Result { } DatabaseBackend::Lmdb => { tracing::info!("Using LMDB backend at: {}", db_path.display()); - // TODO: Implement LMDB backend once nostr-relay-builder supports it - // For now, fall back to memory database - tracing::warn!("LMDB backend not yet implemented, using in-memory database"); - Arc::new(MemoryDatabase::with_opts(MemoryDatabaseOptions { - events: true, - max_events: Some(100_000), - })) + // Ensure the database directory exists + std::fs::create_dir_all(db_path).map_err(|e| { + anyhow::anyhow!("Failed to create LMDB directory {}: {}", db_path.display(), e) + })?; + Arc::new(NostrLMDB::open(db_path).map_err(|e| { + anyhow::anyhow!("Failed to open LMDB database at {}: {}", db_path.display(), e) + })?) } }; -- cgit v1.2.3