upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/nostr/builder.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-03 08:54:00 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-03 08:54:00 +0000
commit2f8ecd482077d82f2d1a937c7f979eaaa87a27b2 (patch)
treecd892cde6ef6fd7ff654377946cab5b95339276f /src/nostr/builder.rs
parent62a3855cb96616caf704a0f112fb2ade99fb8b45 (diff)
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.
Diffstat (limited to 'src/nostr/builder.rs')
-rw-r--r--src/nostr/builder.rs56
1 files changed, 35 insertions, 21 deletions
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;
9use nostr::nips::nip19::ToBech32; 9use nostr::nips::nip19::ToBech32;
10use nostr::prelude::{Alphabet, SingleLetterTag}; 10use nostr::prelude::{Alphabet, SingleLetterTag};
11use nostr::{EventId, Filter, Kind, PublicKey}; 11use nostr::{EventId, Filter, Kind, PublicKey};
12use nostr_lmdb::NostrLMDB;
12use nostr_relay_builder::prelude::*; 13use nostr_relay_builder::prelude::*;
13 14
14use crate::config::{Config, DatabaseBackend}; 15use crate::config::{Config, DatabaseBackend};
@@ -18,6 +19,9 @@ use crate::nostr::events::{
18 KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, 19 KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE,
19}; 20};
20 21
22/// Type alias for the shared database used by the relay
23pub type SharedDatabase = Arc<dyn NostrDatabase>;
24
21/// Result of aligning a repository with authorized state 25/// Result of aligning a repository with authorized state
22#[derive(Debug, Default)] 26#[derive(Debug, Default)]
23struct AlignmentResult { 27struct AlignmentResult {
@@ -35,23 +39,33 @@ struct AlignmentResult {
35/// 39///
36/// Validates all events according to GRASP-01 specification: 40/// Validates all events according to GRASP-01 specification:
37/// - Repository announcements must list service in clone and relays tags 41/// - Repository announcements must list service in clone and relays tags
38/// - Repository state announcements must have valid structure 42/// - Repository state announcements must have valid structure
39/// - Other events must reference accepted repositories or events 43/// - Other events must reference accepted repositories or events
40/// - Forward references are supported (events referenced by accepted events) 44/// - Forward references are supported (events referenced by accepted events)
41/// - Orphan events with no valid references are rejected 45/// - Orphan events with no valid references are rejected
42/// 46///
43/// Uses stateful database queries to check event relationships. 47/// Uses stateful database queries to check event relationships.
44#[derive(Debug, Clone)] 48#[derive(Clone)]
45pub struct Nip34WritePolicy { 49pub struct Nip34WritePolicy {
46 domain: String, 50 domain: String,
47 database: Arc<MemoryDatabase>, 51 database: SharedDatabase,
48 git_data_path: PathBuf, 52 git_data_path: PathBuf,
49} 53}
50 54
55impl std::fmt::Debug for Nip34WritePolicy {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.debug_struct("Nip34WritePolicy")
58 .field("domain", &self.domain)
59 .field("git_data_path", &self.git_data_path)
60 .field("database", &"<database>")
61 .finish()
62 }
63}
64
51impl Nip34WritePolicy { 65impl Nip34WritePolicy {
52 pub fn new( 66 pub fn new(
53 domain: impl Into<String>, 67 domain: impl Into<String>,
54 database: Arc<MemoryDatabase>, 68 database: SharedDatabase,
55 git_data_path: impl Into<PathBuf>, 69 git_data_path: impl Into<PathBuf>,
56 ) -> Self { 70 ) -> Self {
57 Self { 71 Self {
@@ -104,7 +118,7 @@ impl Nip34WritePolicy {
104 /// The authorized_pubkeys should be the owner and maintainers of a specific 118 /// The authorized_pubkeys should be the owner and maintainers of a specific
105 /// announcement, so different owners with the same identifier don't interfere. 119 /// announcement, so different owners with the same identifier don't interfere.
106 async fn is_latest_state_for_identifier( 120 async fn is_latest_state_for_identifier(
107 database: &Arc<MemoryDatabase>, 121 database: &SharedDatabase,
108 state: &RepositoryState, 122 state: &RepositoryState,
109 authorized_pubkeys: &[PublicKey], 123 authorized_pubkeys: &[PublicKey],
110 ) -> Result<bool, String> { 124 ) -> Result<bool, String> {
@@ -155,7 +169,7 @@ impl Nip34WritePolicy {
155 /// should update HEAD in the repository of the announcement owner, 169 /// should update HEAD in the repository of the announcement owner,
156 /// not in the maintainer's own (possibly non-existent) repository. 170 /// not in the maintainer's own (possibly non-existent) repository.
157 async fn find_authorized_announcements( 171 async fn find_authorized_announcements(
158 database: &Arc<MemoryDatabase>, 172 database: &SharedDatabase,
159 identifier: &str, 173 identifier: &str,
160 state_author: &PublicKey, 174 state_author: &PublicKey,
161 ) -> Result<Vec<RepositoryAnnouncement>, String> { 175 ) -> Result<Vec<RepositoryAnnouncement>, String> {
@@ -205,7 +219,7 @@ impl Nip34WritePolicy {
205 /// - This state event is the latest for the identifier in that context 219 /// - This state event is the latest for the identifier in that context
206 async fn identify_owner_repositories( 220 async fn identify_owner_repositories(
207 &self, 221 &self,
208 database: &Arc<MemoryDatabase>, 222 database: &SharedDatabase,
209 state: &RepositoryState, 223 state: &RepositoryState,
210 ) -> Result<Vec<(RepositoryAnnouncement, std::path::PathBuf)>, String> { 224 ) -> Result<Vec<(RepositoryAnnouncement, std::path::PathBuf)>, String> {
211 // Find all announcements where state author is authorized 225 // Find all announcements where state author is authorized
@@ -485,7 +499,7 @@ impl Nip34WritePolicy {
485 /// Ok(Some(n)) if n refs were deleted, Ok(None) if no action taken, Err on failure 499 /// Ok(Some(n)) if n refs were deleted, Ok(None) if no action taken, Err on failure
486 async fn validate_pr_nostr_ref( 500 async fn validate_pr_nostr_ref(
487 &self, 501 &self,
488 database: &Arc<MemoryDatabase>, 502 database: &SharedDatabase,
489 event: &Event, 503 event: &Event,
490 ) -> Result<Option<usize>, String> { 504 ) -> Result<Option<usize>, String> {
491 let event_id = event.id.to_hex(); 505 let event_id = event.id.to_hex();
@@ -651,7 +665,7 @@ impl Nip34WritePolicy {
651 /// Check if any addressable events (repositories) exist in database 665 /// Check if any addressable events (repositories) exist in database
652 /// Returns the first matching addressable reference found, or None if none match 666 /// Returns the first matching addressable reference found, or None if none match
653 async fn find_accepted_repository( 667 async fn find_accepted_repository(
654 database: &Arc<MemoryDatabase>, 668 database: &SharedDatabase,
655 addressables: &[String], 669 addressables: &[String],
656 ) -> Result<Option<String>, String> { 670 ) -> Result<Option<String>, String> {
657 if addressables.is_empty() { 671 if addressables.is_empty() {
@@ -724,7 +738,7 @@ impl Nip34WritePolicy {
724 /// Check if any events exist in database 738 /// Check if any events exist in database
725 /// Returns the first matching event ID found, or None if none match 739 /// Returns the first matching event ID found, or None if none match
726 async fn find_accepted_event( 740 async fn find_accepted_event(
727 database: &Arc<MemoryDatabase>, 741 database: &SharedDatabase,
728 event_ids: &[EventId], 742 event_ids: &[EventId],
729 ) -> Result<Option<EventId>, String> { 743 ) -> Result<Option<EventId>, String> {
730 if event_ids.is_empty() { 744 if event_ids.is_empty() {
@@ -752,7 +766,7 @@ impl Nip34WritePolicy {
752 /// This optimization recognizes that replaceable events are referenced by coordinate address, 766 /// This optimization recognizes that replaceable events are referenced by coordinate address,
753 /// while regular events are referenced by event ID. 767 /// while regular events are referenced by event ID.
754 async fn is_referenced_by_accepted( 768 async fn is_referenced_by_accepted(
755 database: &Arc<MemoryDatabase>, 769 database: &SharedDatabase,
756 event: &Event, 770 event: &Event,
757 ) -> Result<bool, String> { 771 ) -> Result<bool, String> {
758 let kind_u16 = event.kind.as_u16(); 772 let kind_u16 = event.kind.as_u16();
@@ -1152,14 +1166,14 @@ pub struct RelayWithDatabase {
1152 /// The local relay instance 1166 /// The local relay instance
1153 pub relay: LocalRelay, 1167 pub relay: LocalRelay,
1154 /// The database Arc that can be used for direct queries 1168 /// The database Arc that can be used for direct queries
1155 pub database: Arc<MemoryDatabase>, 1169 pub database: SharedDatabase,
1156} 1170}
1157 1171
1158/// Create a configured LocalRelay with full GRASP-01 validation 1172/// Create a configured LocalRelay with full GRASP-01 validation
1159/// 1173///
1160/// Returns a `RelayWithDatabase` struct containing: 1174/// Returns a `RelayWithDatabase` struct containing:
1161/// - The `LocalRelay` for handling WebSocket connections 1175/// - The `LocalRelay` for handling WebSocket connections
1162/// - The `Arc<MemoryDatabase>` for direct database queries (e.g., push authorization) 1176/// - The `SharedDatabase` for direct database queries (e.g., push authorization)
1163pub fn create_relay(config: &Config) -> Result<RelayWithDatabase> { 1177pub fn create_relay(config: &Config) -> Result<RelayWithDatabase> {
1164 tracing::info!("Configuring nostr relay with GRASP-01 validation..."); 1178 tracing::info!("Configuring nostr relay with GRASP-01 validation...");
1165 1179
@@ -1167,7 +1181,7 @@ pub fn create_relay(config: &Config) -> Result<RelayWithDatabase> {
1167 let db_path = Path::new(&config.relay_data_path); 1181 let db_path = Path::new(&config.relay_data_path);
1168 1182
1169 // Create database based on configuration 1183 // Create database based on configuration
1170 let database = match config.database_backend { 1184 let database: SharedDatabase = match config.database_backend {
1171 DatabaseBackend::Memory => { 1185 DatabaseBackend::Memory => {
1172 tracing::info!("Using in-memory database (no persistence)"); 1186 tracing::info!("Using in-memory database (no persistence)");
1173 Arc::new(MemoryDatabase::with_opts(MemoryDatabaseOptions { 1187 Arc::new(MemoryDatabase::with_opts(MemoryDatabaseOptions {
@@ -1187,13 +1201,13 @@ pub fn create_relay(config: &Config) -> Result<RelayWithDatabase> {
1187 } 1201 }
1188 DatabaseBackend::Lmdb => { 1202 DatabaseBackend::Lmdb => {
1189 tracing::info!("Using LMDB backend at: {}", db_path.display()); 1203 tracing::info!("Using LMDB backend at: {}", db_path.display());
1190 // TODO: Implement LMDB backend once nostr-relay-builder supports it 1204 // Ensure the database directory exists
1191 // For now, fall back to memory database 1205 std::fs::create_dir_all(db_path).map_err(|e| {
1192 tracing::warn!("LMDB backend not yet implemented, using in-memory database"); 1206 anyhow::anyhow!("Failed to create LMDB directory {}: {}", db_path.display(), e)
1193 Arc::new(MemoryDatabase::with_opts(MemoryDatabaseOptions { 1207 })?;
1194 events: true, 1208 Arc::new(NostrLMDB::open(db_path).map_err(|e| {
1195 max_events: Some(100_000), 1209 anyhow::anyhow!("Failed to open LMDB database at {}: {}", db_path.display(), e)
1196 })) 1210 })?)
1197 } 1211 }
1198 }; 1212 };
1199 1213