diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-20 23:44:05 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-21 03:05:41 +0000 |
| commit | 2bbb7292c978d36464b6166faa78223677389ef6 (patch) | |
| tree | ef82eb6f9267c27a08e758d32112a33c13991717 | |
| parent | 519fdc66930280cd1772417dca327ed858333d64 (diff) | |
Implement GRASP-01 stateful write policy with database queries
- Add Nip34WritePolicy with Arc<MemoryDatabase> for stateful event validation
- Implement full GRASP-01 event acceptance policy:
* Accept events referencing accepted repositories (via a, A, q tags)
* Accept events referencing accepted events (transitive, via e, E, q tags)
* Support forward references (events referenced by accepted events)
* Reject orphan events with no valid references
- Extract and validate all reference tag types (a, A, q, e, E)
- Query database for repository and event existence checks
- Implement fail-secure error handling for database query failures
Test improvements:
- Fix send_and_verify_rejected to handle relay rejection errors properly
- Fix RepoWithIssue fixture usage in forward reference tests
- Add database synchronization polling for race condition mitigation
- Achieve 94% test pass rate (16/17 integration tests passing)
| -rw-r--r-- | grasp-audit/src/specs/grasp01/event_acceptance_policy.rs | 76 | ||||
| -rw-r--r-- | src/nostr/builder.rs | 270 |
2 files changed, 289 insertions, 57 deletions
diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs index 638ae5f..c1977f9 100644 --- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs +++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs | |||
| @@ -551,11 +551,36 @@ impl EventAcceptancePolicyTests { | |||
| 551 | ) -> Result<(), String> { | 551 | ) -> Result<(), String> { |
| 552 | let event_id = event.id; | 552 | let event_id = event.id; |
| 553 | 553 | ||
| 554 | client | 554 | // Try to send event - rejection may cause send_event to fail with an error |
| 555 | .send_event(event) | 555 | let send_result = client.send_event(event).await; |
| 556 | .await | 556 | |
| 557 | .map_err(|e| format!("Failed to send event to relay: {}", e))?; | 557 | // If send succeeded, the relay might have accepted it (we'll verify below) |
| 558 | // If send failed, check if it's a rejection error (expected) | ||
| 559 | if let Err(e) = send_result { | ||
| 560 | let err_msg = e.to_string().to_lowercase(); | ||
| 561 | // Check if error message indicates rejection (not network/other errors) | ||
| 562 | if err_msg.contains("rejected") || err_msg.contains("blocked") { | ||
| 563 | // Expected rejection - verify event is NOT in database | ||
| 564 | tokio::time::sleep(Duration::from_millis(100)).await; | ||
| 565 | |||
| 566 | let filter = Filter::new().id(event_id); | ||
| 567 | let events = client | ||
| 568 | .query(filter) | ||
| 569 | .await | ||
| 570 | .map_err(|e| format!("Failed to query relay for verification: {}", e))?; | ||
| 571 | |||
| 572 | if !events.is_empty() { | ||
| 573 | return Err(format!("Event was rejected but still stored: {}", description)); | ||
| 574 | } | ||
| 575 | |||
| 576 | return Ok(()); // Rejected as expected | ||
| 577 | } else { | ||
| 578 | // Unexpected error (network, etc.) | ||
| 579 | return Err(format!("Failed to send event to relay: {}", e)); | ||
| 580 | } | ||
| 581 | } | ||
| 558 | 582 | ||
| 583 | // Send succeeded, verify event was NOT stored (relay should have rejected) | ||
| 559 | tokio::time::sleep(Duration::from_millis(100)).await; | 584 | tokio::time::sleep(Duration::from_millis(100)).await; |
| 560 | 585 | ||
| 561 | let filter = Filter::new().id(event_id); | 586 | let filter = Filter::new().id(event_id); |
| @@ -877,6 +902,26 @@ impl EventAcceptancePolicyTests { | |||
| 877 | ) | 902 | ) |
| 878 | })?; | 903 | })?; |
| 879 | 904 | ||
| 905 | // Verify repo is queryable (ensures it's fully indexed before we reference it) | ||
| 906 | let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; | ||
| 907 | let verify_filter = Filter::new() | ||
| 908 | .kind(Kind::GitRepoAnnouncement) | ||
| 909 | .author(repo.pubkey) | ||
| 910 | .identifier(repo_id); | ||
| 911 | |||
| 912 | // Poll until repo is available (with timeout) | ||
| 913 | for _ in 0..10 { | ||
| 914 | let events = client.query(verify_filter.clone()).await | ||
| 915 | .map_err(|e| format!("Failed to verify repo: {}", e))?; | ||
| 916 | if !events.is_empty() { | ||
| 917 | break; | ||
| 918 | } | ||
| 919 | tokio::time::sleep(Duration::from_millis(50)).await; | ||
| 920 | } | ||
| 921 | |||
| 922 | // Extra delay to ensure relay's internal database is fully synchronized | ||
| 923 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 924 | |||
| 880 | // Create Kind 1 note locally but DON'T send it yet | 925 | // Create Kind 1 note locally but DON'T send it yet |
| 881 | let kind1_note = client | 926 | let kind1_note = client |
| 882 | .event_builder(Kind::TextNote, "Note to be referenced") | 927 | .event_builder(Kind::TextNote, "Note to be referenced") |
| @@ -938,34 +983,17 @@ impl EventAcceptancePolicyTests { | |||
| 938 | // Create TestContext | 983 | // Create TestContext |
| 939 | let ctx = TestContext::new(client); | 984 | let ctx = TestContext::new(client); |
| 940 | 985 | ||
| 941 | // Get repo with issue fixture (mode-aware) | 986 | // Get issue fixture (mode-aware) - RepoWithIssue returns the issue event directly |
| 942 | let repo = ctx | 987 | let issue = ctx |
| 943 | .get_fixture(FixtureKind::RepoWithIssue) | 988 | .get_fixture(FixtureKind::RepoWithIssue) |
| 944 | .await | 989 | .await |
| 945 | .map_err(|e| { | 990 | .map_err(|e| { |
| 946 | format!( | 991 | format!( |
| 947 | "Test setup failed: could not get repo with issue fixture: {}", | 992 | "Test setup failed: could not get issue fixture: {}", |
| 948 | e | 993 | e |
| 949 | ) | 994 | ) |
| 950 | })?; | 995 | })?; |
| 951 | 996 | ||
| 952 | // Extract the issue from the repo event (it's stored as the first 'e' tag) | ||
| 953 | let issue_id = repo | ||
| 954 | .tags | ||
| 955 | .iter() | ||
| 956 | .find(|t| t.kind() == TagKind::e()) | ||
| 957 | .and_then(|t| t.content()) | ||
| 958 | .ok_or("Missing issue reference in RepoWithIssue fixture")?; | ||
| 959 | |||
| 960 | // Query to get the actual issue event | ||
| 961 | let filter = Filter::new().id(nostr_sdk::EventId::from_hex(issue_id) | ||
| 962 | .map_err(|e| format!("Invalid issue ID: {}", e))?); | ||
| 963 | let issues = client | ||
| 964 | .query(filter) | ||
| 965 | .await | ||
| 966 | .map_err(|e| format!("Failed to query issue: {}", e))?; | ||
| 967 | let issue = issues.first().ok_or("Issue not found")?.clone(); | ||
| 968 | |||
| 969 | // Create Comment A locally but DON'T send it yet | 997 | // Create Comment A locally but DON'T send it yet |
| 970 | let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?; | 998 | let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?; |
| 971 | 999 | ||
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index cd1f4d2..7a35bb7 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs | |||
| @@ -4,8 +4,11 @@ | |||
| 4 | /// preserved from the original implementation. | 4 | /// preserved from the original implementation. |
| 5 | use std::net::SocketAddr; | 5 | use std::net::SocketAddr; |
| 6 | use std::path::Path; | 6 | use std::path::Path; |
| 7 | use std::sync::Arc; | ||
| 7 | 8 | ||
| 8 | use nostr::nips::nip19::ToBech32; | 9 | use nostr::nips::nip19::ToBech32; |
| 10 | use nostr::prelude::{Alphabet, SingleLetterTag}; | ||
| 11 | use nostr::{EventId, Filter, Kind, PublicKey}; | ||
| 9 | use nostr_relay_builder::prelude::*; | 12 | use nostr_relay_builder::prelude::*; |
| 10 | 13 | ||
| 11 | use crate::config::Config; | 14 | use crate::config::Config; |
| @@ -13,20 +16,141 @@ use crate::nostr::events::{ | |||
| 13 | validate_announcement, validate_state, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, | 16 | validate_announcement, validate_state, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, |
| 14 | }; | 17 | }; |
| 15 | 18 | ||
| 16 | /// NIP-34 Write Policy | 19 | /// NIP-34 Write Policy with Full GRASP-01 Event Validation |
| 17 | /// | 20 | /// |
| 18 | /// Validates repository announcement and state events according to GRASP-01 spec. | 21 | /// Validates all events according to GRASP-01 specification: |
| 19 | /// Preserves all original validation logic from src/nostr/events.rs. | 22 | /// - Repository announcements must list service in clone and relays tags |
| 23 | /// - Repository state announcements must have valid structure | ||
| 24 | /// - Other events must reference accepted repositories or events | ||
| 25 | /// - Forward references are supported (events referenced by accepted events) | ||
| 26 | /// - Orphan events with no valid references are rejected | ||
| 27 | /// | ||
| 28 | /// Uses stateful database queries to check event relationships. | ||
| 20 | #[derive(Debug, Clone)] | 29 | #[derive(Debug, Clone)] |
| 21 | pub struct Nip34WritePolicy { | 30 | pub struct Nip34WritePolicy { |
| 22 | domain: String, | 31 | domain: String, |
| 32 | database: Arc<MemoryDatabase>, | ||
| 23 | } | 33 | } |
| 24 | 34 | ||
| 25 | impl Nip34WritePolicy { | 35 | impl Nip34WritePolicy { |
| 26 | pub fn new(domain: impl Into<String>) -> Self { | 36 | pub fn new(domain: impl Into<String>, database: Arc<MemoryDatabase>) -> Self { |
| 27 | Self { | 37 | Self { |
| 28 | domain: domain.into(), | 38 | domain: domain.into(), |
| 39 | database, | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | /// Extract all reference tags from an event (a, A, q, e, E) | ||
| 44 | /// Returns (addressable_refs, event_refs) | ||
| 45 | fn extract_reference_tags(event: &Event) -> (Vec<String>, Vec<EventId>) { | ||
| 46 | let mut addressable_refs = Vec::new(); | ||
| 47 | let mut event_refs = Vec::new(); | ||
| 48 | |||
| 49 | for tag in event.tags.iter() { | ||
| 50 | let tag_vec = tag.clone().to_vec(); | ||
| 51 | if tag_vec.is_empty() { | ||
| 52 | continue; | ||
| 53 | } | ||
| 54 | |||
| 55 | match tag_vec[0].as_str() { | ||
| 56 | // Addressable event references (a, A, q with kind:pubkey:identifier format) | ||
| 57 | "a" | "A" | "q" if tag_vec.len() > 1 && tag_vec[1].contains(':') => { | ||
| 58 | addressable_refs.push(tag_vec[1].clone()); | ||
| 59 | } | ||
| 60 | // Event ID references (e, E, q with event ID format) | ||
| 61 | "e" | "E" if tag_vec.len() > 1 => { | ||
| 62 | if let Ok(event_id) = EventId::from_hex(&tag_vec[1]) { | ||
| 63 | event_refs.push(event_id); | ||
| 64 | } | ||
| 65 | } | ||
| 66 | "q" if tag_vec.len() > 1 && !tag_vec[1].contains(':') => { | ||
| 67 | if let Ok(event_id) = EventId::from_hex(&tag_vec[1]) { | ||
| 68 | event_refs.push(event_id); | ||
| 69 | } | ||
| 70 | } | ||
| 71 | _ => {} | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | (addressable_refs, event_refs) | ||
| 76 | } | ||
| 77 | |||
| 78 | /// Check if an addressable event (repository) exists in database | ||
| 79 | async fn is_accepted_repository( | ||
| 80 | database: &Arc<MemoryDatabase>, | ||
| 81 | addressable: &str, | ||
| 82 | ) -> Result<bool, String> { | ||
| 83 | // Parse addressable format: kind:pubkey:identifier | ||
| 84 | let parts: Vec<&str> = addressable.split(':').collect(); | ||
| 85 | if parts.len() < 3 { | ||
| 86 | return Ok(false); | ||
| 87 | } | ||
| 88 | |||
| 89 | let kind = parts[0] | ||
| 90 | .parse::<u16>() | ||
| 91 | .map_err(|e| format!("Invalid kind in addressable: {}", e))?; | ||
| 92 | let pubkey = PublicKey::from_hex(parts[1]) | ||
| 93 | .map_err(|e| format!("Invalid pubkey in addressable: {}", e))?; | ||
| 94 | let identifier = parts[2]; | ||
| 95 | |||
| 96 | // Query database for this addressable event | ||
| 97 | let filter = Filter::new() | ||
| 98 | .kind(Kind::from(kind)) | ||
| 99 | .author(pubkey) | ||
| 100 | .identifier(identifier); | ||
| 101 | |||
| 102 | match database.query(filter).await { | ||
| 103 | Ok(events) => Ok(!events.is_empty()), | ||
| 104 | Err(e) => Err(format!("Database query failed: {}", e)), | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | /// Check if an event exists in database | ||
| 109 | async fn is_accepted_event( | ||
| 110 | database: &Arc<MemoryDatabase>, | ||
| 111 | event_id: &EventId, | ||
| 112 | ) -> Result<bool, String> { | ||
| 113 | let filter = Filter::new().id(*event_id); | ||
| 114 | |||
| 115 | match database.query(filter).await { | ||
| 116 | Ok(events) => Ok(!events.is_empty()), | ||
| 117 | Err(e) => Err(format!("Database query failed: {}", e)), | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | /// Check if any accepted event references this event ID (forward reference) | ||
| 122 | /// Checks all reference tag types: e, E, q, Q (event ID references only, not addressable) | ||
| 123 | /// | ||
| 124 | /// Note: Must check each tag type separately as custom_tag chaining creates AND not OR | ||
| 125 | async fn is_referenced_by_accepted( | ||
| 126 | database: &Arc<MemoryDatabase>, | ||
| 127 | event_id: &EventId, | ||
| 128 | ) -> Result<bool, String> { | ||
| 129 | let event_id_hex = event_id.to_hex(); | ||
| 130 | |||
| 131 | // Check each tag type that can reference event IDs | ||
| 132 | // Note: 'a' and 'A' tags use addressable format (kind:pubkey:d), not event IDs | ||
| 133 | let tag_types = [ | ||
| 134 | SingleLetterTag::lowercase(Alphabet::E), // 'e' - standard event reference | ||
| 135 | SingleLetterTag::uppercase(Alphabet::E), // 'E' - NIP-22 root event reference | ||
| 136 | SingleLetterTag::lowercase(Alphabet::Q), // 'q' - quote reference | ||
| 137 | SingleLetterTag::uppercase(Alphabet::Q), // 'Q' - uppercase quote (if used) | ||
| 138 | ]; | ||
| 139 | |||
| 140 | for tag_type in &tag_types { | ||
| 141 | let filter = Filter::new().custom_tag(tag_type.clone(), event_id_hex.clone()); | ||
| 142 | |||
| 143 | match database.query(filter).await { | ||
| 144 | Ok(events) => { | ||
| 145 | if !events.is_empty() { | ||
| 146 | return Ok(true); | ||
| 147 | } | ||
| 148 | } | ||
| 149 | Err(e) => return Err(format!("Database query failed: {}", e)), | ||
| 150 | } | ||
| 29 | } | 151 | } |
| 152 | |||
| 153 | Ok(false) | ||
| 30 | } | 154 | } |
| 31 | } | 155 | } |
| 32 | 156 | ||
| @@ -36,86 +160,166 @@ impl WritePolicy for Nip34WritePolicy { | |||
| 36 | event: &'a nostr_relay_builder::prelude::Event, | 160 | event: &'a nostr_relay_builder::prelude::Event, |
| 37 | _addr: &'a SocketAddr, | 161 | _addr: &'a SocketAddr, |
| 38 | ) -> BoxedFuture<'a, PolicyResult> { | 162 | ) -> BoxedFuture<'a, PolicyResult> { |
| 163 | let database = self.database.clone(); | ||
| 164 | let domain = self.domain.clone(); | ||
| 165 | |||
| 39 | Box::pin(async move { | 166 | Box::pin(async move { |
| 167 | let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); | ||
| 168 | |||
| 40 | match event.kind.as_u16() { | 169 | match event.kind.as_u16() { |
| 41 | KIND_REPOSITORY_ANNOUNCEMENT => match validate_announcement(event, &self.domain) { | 170 | KIND_REPOSITORY_ANNOUNCEMENT => match validate_announcement(event, &domain) { |
| 42 | Ok(_) => { | 171 | Ok(_) => { |
| 43 | tracing::debug!( | 172 | tracing::debug!( |
| 44 | "Accepted repository announcement: {}", | 173 | "Accepted repository announcement: {}", |
| 45 | event | 174 | event_id_str |
| 46 | .id | ||
| 47 | .to_bech32() | ||
| 48 | .unwrap_or_else(|_| "invalid".to_string()) | ||
| 49 | ); | 175 | ); |
| 50 | PolicyResult::Accept | 176 | PolicyResult::Accept |
| 51 | } | 177 | } |
| 52 | Err(e) => { | 178 | Err(e) => { |
| 53 | tracing::warn!( | 179 | tracing::warn!( |
| 54 | "Rejected repository announcement {}: {}", | 180 | "Rejected repository announcement {}: {}", |
| 55 | event | 181 | event_id_str, |
| 56 | .id | ||
| 57 | .to_bech32() | ||
| 58 | .unwrap_or_else(|_| "invalid".to_string()), | ||
| 59 | e | 182 | e |
| 60 | ); | 183 | ); |
| 61 | PolicyResult::Reject(e.to_string()) | 184 | PolicyResult::Reject(e.to_string()) |
| 62 | } | 185 | } |
| 63 | }, | 186 | }, |
| 64 | KIND_REPOSITORY_STATE => match validate_state(event) { | 187 | KIND_REPOSITORY_STATE =>match validate_state(event) { |
| 65 | Ok(_) => { | 188 | Ok(_) => { |
| 66 | tracing::debug!( | 189 | tracing::debug!( |
| 67 | "Accepted repository state: {}", | 190 | "Accepted repository state: {}", |
| 68 | event | 191 | event_id_str |
| 69 | .id | ||
| 70 | .to_bech32() | ||
| 71 | .unwrap_or_else(|_| "invalid".to_string()) | ||
| 72 | ); | 192 | ); |
| 73 | PolicyResult::Accept | 193 | PolicyResult::Accept |
| 74 | } | 194 | } |
| 75 | Err(e) => { | 195 | Err(e) => { |
| 76 | tracing::warn!( | 196 | tracing::warn!( |
| 77 | "Rejected repository state {}: {}", | 197 | "Rejected repository state {}: {}", |
| 78 | event | 198 | event_id_str, |
| 79 | .id | ||
| 80 | .to_bech32() | ||
| 81 | .unwrap_or_else(|_| "invalid".to_string()), | ||
| 82 | e | 199 | e |
| 83 | ); | 200 | ); |
| 84 | PolicyResult::Reject(e.to_string()) | 201 | PolicyResult::Reject(e.to_string()) |
| 85 | } | 202 | } |
| 86 | }, | 203 | }, |
| 87 | // Accept all other event kinds without validation | 204 | // GRASP-01: Check if event references accepted repositories or events |
| 88 | _ => PolicyResult::Accept, | 205 | _ => { |
| 206 | // Extract all reference tags from event | ||
| 207 | let (addressable_refs, event_refs) = Self::extract_reference_tags(event); | ||
| 208 | |||
| 209 | // Check 1: Does this event reference an accepted repository? | ||
| 210 | for addr_ref in &addressable_refs { | ||
| 211 | match Self::is_accepted_repository(&database, addr_ref).await { | ||
| 212 | Ok(true) => { | ||
| 213 | tracing::debug!( | ||
| 214 | "Accepted event {}: references accepted repository {}", | ||
| 215 | event_id_str, | ||
| 216 | addr_ref | ||
| 217 | ); | ||
| 218 | return PolicyResult::Accept; | ||
| 219 | } | ||
| 220 | Ok(false) => { | ||
| 221 | // Continue checking other references | ||
| 222 | } | ||
| 223 | Err(e) => { | ||
| 224 | tracing::warn!( | ||
| 225 | "Database query failed for event {}, rejecting (fail-secure): {}", | ||
| 226 | event_id_str, | ||
| 227 | e | ||
| 228 | ); | ||
| 229 | return PolicyResult::Reject(format!("Database query failed: {}", e)); | ||
| 230 | } | ||
| 231 | } | ||
| 232 | } | ||
| 233 | |||
| 234 | // Check 2: Does this event reference an accepted event? (transitive) | ||
| 235 | for event_ref in &event_refs { | ||
| 236 | match Self::is_accepted_event(&database, event_ref).await { | ||
| 237 | Ok(true) => { | ||
| 238 | tracing::debug!( | ||
| 239 | "Accepted event {}: references accepted event {}", | ||
| 240 | event_id_str, | ||
| 241 | event_ref | ||
| 242 | ); | ||
| 243 | return PolicyResult::Accept; | ||
| 244 | } | ||
| 245 | Ok(false) => { | ||
| 246 | // Continue checking other references | ||
| 247 | } | ||
| 248 | Err(e) => { | ||
| 249 | tracing::warn!( | ||
| 250 | "Database query failed for event {}, rejecting (fail-secure): {}", | ||
| 251 | event_id_str, | ||
| 252 | e | ||
| 253 | ); | ||
| 254 | return PolicyResult::Reject(format!("Database query failed: {}", e)); | ||
| 255 | } | ||
| 256 | } | ||
| 257 | } | ||
| 258 | |||
| 259 | // Check 3: Is this event referenced by an accepted event? (forward reference) | ||
| 260 | match Self::is_referenced_by_accepted(&database, &event.id).await { | ||
| 261 | Ok(true) => { | ||
| 262 | tracing::debug!( | ||
| 263 | "Accepted event {}: referenced by accepted event", | ||
| 264 | event_id_str | ||
| 265 | ); | ||
| 266 | return PolicyResult::Accept; | ||
| 267 | } | ||
| 268 | Ok(false) => { | ||
| 269 | // No forward references found, continue to rejection | ||
| 270 | } | ||
| 271 | Err(e) => { | ||
| 272 | tracing::warn!( | ||
| 273 | "Database query failed for event {}, rejecting (fail-secure): {}", | ||
| 274 | event_id_str, | ||
| 275 | e | ||
| 276 | ); | ||
| 277 | return PolicyResult::Reject(format!("Database query failed: {}", e)); | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | // No valid references found - reject as orphan event | ||
| 282 | tracing::info!( | ||
| 283 | "Rejected orphan event {}: no references to accepted repos or events (checked {} addressable, {} event refs)", | ||
| 284 | event_id_str, | ||
| 285 | addressable_refs.len(), | ||
| 286 | event_refs.len() | ||
| 287 | ); | ||
| 288 | PolicyResult::Reject( | ||
| 289 | "Event must reference an accepted repository or accepted event".to_string() | ||
| 290 | ) | ||
| 291 | } | ||
| 89 | } | 292 | } |
| 90 | }) | 293 | }) |
| 91 | } | 294 | } |
| 92 | } | 295 | } |
| 93 | 296 | ||
| 94 | /// Create a configured LocalRelay with NIP-34 validation | 297 | /// Create a configured LocalRelay with full GRASP-01 validation |
| 95 | pub fn create_relay(config: &Config) -> Result<LocalRelay> { | 298 | pub fn create_relay(config: &Config) -> Result<LocalRelay> { |
| 96 | tracing::info!("Configuring nostr relay..."); | 299 | tracing::info!("Configuring nostr relay with GRASP-01 validation..."); |
| 97 | 300 | ||
| 98 | // Determine database path | 301 | // Determine database path |
| 99 | let db_path = Path::new(&config.relay_data_path); | 302 | let db_path = Path::new(&config.relay_data_path); |
| 100 | 303 | ||
| 101 | // Create database - using in-memory for now, can switch to persistent later | 304 | // Create database - using in-memory for now, can switch to persistent later |
| 102 | // TODO: Add configuration for NostrDB or LMDB backends | 305 | // TODO: Add configuration for NostrDB or LMDB backends |
| 103 | let database = MemoryDatabase::with_opts(MemoryDatabaseOptions { | 306 | let database = Arc::new(MemoryDatabase::with_opts(MemoryDatabaseOptions { |
| 104 | events: true, | 307 | events: true, |
| 105 | max_events: Some(100_000), | 308 | max_events: Some(100_000), |
| 106 | }); | 309 | })); |
| 107 | 310 | ||
| 108 | tracing::info!("Using in-memory database (path: {})", db_path.display()); | 311 | tracing::info!("Using in-memory database (path: {})", db_path.display()); |
| 109 | 312 | ||
| 110 | // Build relay with NIP-34 validation | 313 | // Build relay with GRASP-01 validation |
| 314 | // Clone Arc for the write policy so both relay and policy can access the database | ||
| 111 | let builder = RelayBuilder::default() | 315 | let builder = RelayBuilder::default() |
| 112 | .database(database) | 316 | .database(database.clone()) |
| 113 | .write_policy(Nip34WritePolicy::new(&config.domain)); | 317 | .write_policy(Nip34WritePolicy::new(&config.domain, database.clone())); |
| 114 | 318 | ||
| 115 | tracing::info!( | 319 | tracing::info!( |
| 116 | "Relay configured with NIP-34 validation for domain: {}", | 320 | "Relay configured with GRASP-01 validation for domain: {}", |
| 117 | config.domain | 321 | config.domain |
| 118 | ); | 322 | ); |
| 119 | 323 | ||
| 120 | Ok(LocalRelay::new(builder)) | 324 | Ok(LocalRelay::new(builder)) |
| 121 | } | 325 | } \ No newline at end of file |