Files
ngit-grasp/src/nostr/policy/related.rs
T
DanConwayDev 819866330c refactor: split Nip34WritePolicy into focused sub-policies
Split the ~900 line Nip34WritePolicy into focused sub-policies for improved
testability and maintainability:

- AnnouncementPolicy - Repository announcement validation
- StatePolicy - State event validation + ref alignment
- PrEventPolicy - PR/PR Update validation
- RelatedEventPolicy - Forward/backward reference checking

The main Nip34WritePolicy now delegates to these sub-policies via a shared
PolicyContext that provides domain, database, and git_data_path.

Also updates:
- README.md: Accurate project structure reflecting actual implementation
- docs/learnings: Marks this technical debt item as complete
2025-12-04 15:42:00 +00:00

276 lines
10 KiB
Rust

/// Related Event Policy - Forward/backward reference checking
///
/// Handles validation of events that reference accepted repositories or events
/// (backward references) and events that are referenced by accepted events
/// (forward references).
use nostr_relay_builder::prelude::{
Alphabet, Event, EventId, Filter, Kind, PublicKey, SingleLetterTag,
};
use super::PolicyContext;
/// Result of reference checking
#[derive(Debug)]
pub enum ReferenceResult {
/// Event references an accepted repository (addressable ref found)
ReferencesRepository(String),
/// Event references an accepted event (event ID found)
ReferencesEvent(EventId),
/// Event is referenced by an accepted event (forward reference)
ReferencedByAccepted,
/// No valid references found - event is an orphan
Orphan,
}
/// Policy for checking event references (backward and forward)
#[derive(Clone)]
pub struct RelatedEventPolicy {
ctx: PolicyContext,
}
impl RelatedEventPolicy {
pub fn new(ctx: PolicyContext) -> Self {
Self { ctx }
}
/// Check all reference types for an event
///
/// Returns the first valid reference found, or `Orphan` if none found.
pub async fn check_references(&self, event: &Event) -> Result<ReferenceResult, String> {
// Extract all reference tags from event
let (addressable_refs, event_refs) = Self::extract_reference_tags(event);
// Check 1: Does this event reference an accepted repository?
if let Some(addr_ref) = self.find_accepted_repository(&addressable_refs).await? {
return Ok(ReferenceResult::ReferencesRepository(addr_ref));
}
// Check 2: Does this event reference an accepted event?
if let Some(event_ref) = self.find_accepted_event(&event_refs).await? {
return Ok(ReferenceResult::ReferencesEvent(event_ref));
}
// Check 3: Is this event referenced by an accepted event?
if self.is_referenced_by_accepted(event).await? {
return Ok(ReferenceResult::ReferencedByAccepted);
}
// No valid references found
Ok(ReferenceResult::Orphan)
}
/// Extract all reference tags from an event (a, A, q, e, E)
/// Returns (addressable_refs, event_refs)
pub fn extract_reference_tags(event: &Event) -> (Vec<String>, Vec<EventId>) {
let mut addressable_refs = Vec::new();
let mut event_refs = Vec::new();
for tag in event.tags.iter() {
let tag_vec = tag.clone().to_vec();
if tag_vec.is_empty() {
continue;
}
match tag_vec[0].as_str() {
// Addressable event references (a, A, q with kind:pubkey:identifier format)
"a" | "A" | "q" if tag_vec.len() > 1 && tag_vec[1].contains(':') => {
addressable_refs.push(tag_vec[1].clone());
}
// Event ID references (e, E, q with event ID format)
"e" | "E" if tag_vec.len() > 1 => {
if let Ok(event_id) = EventId::from_hex(&tag_vec[1]) {
event_refs.push(event_id);
}
}
"q" if tag_vec.len() > 1 && !tag_vec[1].contains(':') => {
if let Ok(event_id) = EventId::from_hex(&tag_vec[1]) {
event_refs.push(event_id);
}
}
_ => {}
}
}
(addressable_refs, event_refs)
}
/// 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(
&self,
addressables: &[String],
) -> Result<Option<String>, String> {
if addressables.is_empty() {
return Ok(None);
}
// Parse all addressable references
let mut parsed_refs = Vec::new();
for addr in addressables {
let parts: Vec<&str> = addr.split(':').collect();
if parts.len() < 3 {
continue; // Skip invalid format
}
let kind = match parts[0].parse::<u16>() {
Ok(k) => k,
Err(_) => continue, // Skip invalid kind
};
let pubkey = match PublicKey::from_hex(parts[1]) {
Ok(pk) => pk,
Err(_) => continue, // Skip invalid pubkey
};
let identifier = parts[2].to_string();
parsed_refs.push((addr.clone(), kind, pubkey, identifier));
}
if parsed_refs.is_empty() {
return Ok(None);
}
// Group by kind to reduce queries
use std::collections::HashMap;
let mut by_kind: HashMap<u16, Vec<_>> = HashMap::new();
for (addr, kind, pubkey, identifier) in parsed_refs {
by_kind
.entry(kind)
.or_default()
.push((addr, pubkey, identifier));
}
// Query each kind group
for (kind, refs) in by_kind {
let authors: Vec<PublicKey> = refs.iter().map(|(_, pk, _)| *pk).collect();
let filter = Filter::new().kind(Kind::from(kind)).authors(authors);
match self.ctx.database.query(filter).await {
Ok(events) => {
// Check if any event matches our identifier requirements
for event in events {
for (addr, _pubkey, identifier) in &refs {
// Match identifier tag
if event.tags.iter().any(|tag| {
let tag_vec = tag.clone().to_vec();
tag_vec.len() >= 2 && tag_vec[0] == "d" && tag_vec[1] == *identifier
}) {
return Ok(Some(addr.clone()));
}
}
}
}
Err(e) => return Err(format!("Database query failed: {}", e)),
}
}
Ok(None)
}
/// Check if any events exist in database
/// Returns the first matching event ID found, or None if none match
async fn find_accepted_event(
&self,
event_ids: &[EventId],
) -> Result<Option<EventId>, String> {
if event_ids.is_empty() {
return Ok(None);
}
// Single query for all event IDs
let filter = Filter::new().ids(event_ids.iter().copied());
match self.ctx.database.query(filter).await {
Ok(events) => {
// Get first event from the iterator
Ok(events.into_iter().next().map(|e| e.id))
}
Err(e) => Err(format!("Database query failed: {}", e)),
}
}
/// Check if any accepted event references this event (forward reference)
///
/// For regular replaceable events (10000-19999): Checks addressable tags with kind:pubkey format
/// For parameterized replaceable (30000-39999): Checks addressable tags with kind:pubkey:d-identifier format
/// For regular events: Only checks event ID reference tags (e, E, q)
async fn is_referenced_by_accepted(&self, event: &Event) -> Result<bool, String> {
let kind_u16 = event.kind.as_u16();
// Check if this is any kind of replaceable event
let is_regular_replaceable = (10000..20000).contains(&kind_u16);
let is_parameterized_replaceable = (30000..40000).contains(&kind_u16);
if is_regular_replaceable || is_parameterized_replaceable {
// Build the appropriate address format based on event type
let address = if is_parameterized_replaceable {
// For parameterized replaceable: kind:pubkey:d-identifier format (2 colons)
let identifier = event
.tags
.iter()
.find_map(|tag| {
let tag_vec = tag.clone().to_vec();
if tag_vec.len() >= 2 && tag_vec[0] == "d" {
Some(tag_vec[1].clone())
} else {
None
}
})
.unwrap_or_default(); // Empty string if no 'd' tag
format!(
"{}:{}:{}",
event.kind.as_u16(),
event.pubkey.to_hex(),
identifier
)
} else {
// For regular replaceable: kind:pubkey format (1 colon)
format!("{}:{}", event.kind.as_u16(), event.pubkey.to_hex())
};
// Check addressable reference tags: a, A, q (with address format)
let addressable_tags = [
SingleLetterTag::lowercase(Alphabet::A), // 'a' - addressable event reference
SingleLetterTag::uppercase(Alphabet::A), // 'A' - uppercase addressable reference
SingleLetterTag::lowercase(Alphabet::Q), // 'q' - quote (can be address or ID)
];
for tag_type in &addressable_tags {
let filter = Filter::new().custom_tag(*tag_type, address.clone());
match self.ctx.database.query(filter).await {
Ok(events) => {
if !events.is_empty() {
return Ok(true);
}
}
Err(e) => return Err(format!("Database query failed: {}", e)),
}
}
} else {
// For regular events, check event ID reference tags: e, E, q (with hex ID)
let event_id_hex = event.id.to_hex();
let event_id_tags = [
SingleLetterTag::lowercase(Alphabet::E), // 'e' - standard event reference
SingleLetterTag::uppercase(Alphabet::E), // 'E' - NIP-22 root event reference
SingleLetterTag::lowercase(Alphabet::Q), // 'q' - quote reference
];
for tag_type in &event_id_tags {
let filter = Filter::new().custom_tag(*tag_type, event_id_hex.clone());
match self.ctx.database.query(filter).await {
Ok(events) => {
if !events.is_empty() {
return Ok(true);
}
}
Err(e) => return Err(format!("Database query failed: {}", e)),
}
}
}
Ok(false)
}
}