upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-31 09:17:49 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-31 09:17:49 +0000
commit3d6901831904141166d9ed8f47813c45cba109b6 (patch)
tree44d0a431b148ad301971fadb7017dfbf937e45ff /src
parent08ab20509b9c730d3db98dd6e9deb5e2b548979e (diff)
purgatory: fix state event receive code
Diffstat (limited to 'src')
-rw-r--r--src/git/authorization.rs1
-rw-r--r--src/git/mod.rs24
-rw-r--r--src/nostr/builder.rs47
-rw-r--r--src/nostr/policy/state.rs168
4 files changed, 149 insertions, 91 deletions
diff --git a/src/git/authorization.rs b/src/git/authorization.rs
index 9bcbdf8..6b997d8 100644
--- a/src/git/authorization.rs
+++ b/src/git/authorization.rs
@@ -149,7 +149,6 @@ pub async fn authorize_push(
149 "Found {} non-refs/nostr/ refs - checking state authorization", 149 "Found {} non-refs/nostr/ refs - checking state authorization",
150 state_refs.len() 150 state_refs.len()
151 ); 151 );
152
153 let auth_result = get_state_authorization_for_specific_owner_repo( 152 let auth_result = get_state_authorization_for_specific_owner_repo(
154 database, 153 database,
155 identifier, 154 identifier,
diff --git a/src/git/mod.rs b/src/git/mod.rs
index 5c99b3e..1847c8c 100644
--- a/src/git/mod.rs
+++ b/src/git/mod.rs
@@ -74,6 +74,30 @@ pub fn commit_exists(repo_path: &Path, commit_hash: &str) -> bool {
74 } 74 }
75} 75}
76 76
77/// Check if a oid exists in the repository
78///
79/// # Arguments
80/// * `repo_path` - Path to the bare git repository
81/// * `oid` - The commit hash to check
82///
83/// # Returns
84/// True if the commit exists in the repository, false otherwise
85pub fn oid_exists(repo_path: &Path, oid: &str) -> bool {
86 let output = Command::new("git")
87 .args(["cat-file", "-e", oid])
88 .current_dir(repo_path)
89 .output();
90
91 match output {
92 Ok(result) => result.status.success(),
93 Err(_) => false,
94 }
95}
96
97pub fn is_valid_oid(oid: &str) -> bool {
98 oid.len() >= 5 && oid.len() <= 40 && oid.chars().all(|c| c.is_digit(16))
99}
100
77/// Set the repository HEAD to point to a branch 101/// Set the repository HEAD to point to a branch
78/// 102///
79/// This updates the HEAD symbolic ref to point to the specified branch. 103/// This updates the HEAD symbolic ref to point to the specified branch.
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index 2b4d524..37fa025 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -144,51 +144,16 @@ impl Nip34WritePolicy {
144 144
145 match self.state_policy.validate(event) { 145 match self.state_policy.validate(event) {
146 StateResult::Accept => { 146 StateResult::Accept => {
147 // Parse state to get identifier for purgatory message
148 let identifier = event
149 .tags
150 .iter()
151 .find_map(|tag| {
152 let tag_vec = tag.clone().to_vec();
153 if tag_vec.len() >= 2 && tag_vec[0] == "d" {
154 Some(tag_vec[1].clone())
155 } else {
156 None
157 }
158 })
159 .unwrap_or_else(|| "unknown".to_string());
160
161 // Process state alignment asynchronously 147 // Process state alignment asynchronously
162 match self.state_policy.process_state_event(event).await { 148 match self.state_policy.process_state_event(event).await {
163 Ok(0) => { 149 Ok(poilicy_result) => poilicy_result,
164 // No repos aligned - event was added to purgatory
165 tracing::info!(
166 "State event {} added to purgatory: waiting for git data for identifier {}",
167 event_id_str,
168 identifier
169 );
170 WritePolicyResult::Reject {
171 status: true, // Client sees OK
172 message: format!(
173 "purgatory: state event stored, waiting for git push for {}",
174 identifier
175 )
176 .into(),
177 }
178 }
179 Ok(count) => {
180 // Successfully aligned repos
181 tracing::debug!(
182 "Accepted repository state {}: aligned {} repo(s)",
183 event_id_str,
184 count
185 );
186 WritePolicyResult::Accept
187 }
188 Err(e) => { 150 Err(e) => {
189 tracing::warn!("Failed to process state event {}: {}", event_id_str, e); 151 tracing::warn!("Failed to process state event {}: {}", event_id_str, e);
190 // Still accept the event even if processing failed 152 // reject if processing failed
191 WritePolicyResult::Accept 153 WritePolicyResult::Reject {
154 status: false,
155 message: format!("error: {e}").into(),
156 }
192 } 157 }
193 } 158 }
194 } 159 }
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs
index 5e749ed..13f2549 100644
--- a/src/nostr/policy/state.rs
+++ b/src/nostr/policy/state.rs
@@ -1,3 +1,7 @@
1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result};
4use nostr_relay_builder::builder::WritePolicyResult;
1/// State Policy - State event validation + ref alignment 5/// State Policy - State event validation + ref alignment
2/// 6///
3/// Handles validation of NIP-34 repository state events (kind 30618) 7/// Handles validation of NIP-34 repository state events (kind 30618)
@@ -5,7 +9,8 @@
5use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag}; 9use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag};
6 10
7use super::PolicyContext; 11use super::PolicyContext;
8use crate::git; 12use crate::git::authorization::{collect_authorized_maintainers, fetch_repository_data};
13use crate::git::{self};
9use crate::nostr::events::{ 14use crate::nostr::events::{
10 validate_state, RepositoryAnnouncement, RepositoryState, KIND_REPOSITORY_ANNOUNCEMENT, 15 validate_state, RepositoryAnnouncement, RepositoryState, KIND_REPOSITORY_ANNOUNCEMENT,
11 KIND_REPOSITORY_STATE, 16 KIND_REPOSITORY_STATE,
@@ -60,66 +65,107 @@ impl StatePolicy {
60 65
61 /// Process a state event: validate and align owner repositories 66 /// Process a state event: validate and align owner repositories
62 /// 67 ///
63 /// Returns the number of repositories aligned if successful. 68 /// Returns the true if git data already availale or false if added to purgatory
64 pub async fn process_state_event(&self, event: &Event) -> Result<usize, String> { 69 pub async fn process_state_event(&self, event: &Event) -> Result<WritePolicyResult> {
65 // Parse state to get HEAD and branch info 70 // Parse state to get HEAD and branch info
66 let state = RepositoryState::from_event(event.clone()) 71 let state =
67 .map_err(|e| format!("Failed to parse state: {}", e))?; 72 RepositoryState::from_event(event.clone()).context("Failed to parse state event")?;
68 73
69 // Check if ANY git repositories exist for this identifier (regardless of authorization) 74 // duplicate check in purgatory
70 // This helps us distinguish "no git data yet" from "not authorized" or "not latest" 75 if self
71 let has_any_git_data = self.has_git_data_for_identifier(&state.identifier); 76 .ctx
72 77 .purgatory
73 if !has_any_git_data { 78 .find_state(&state.identifier)
74 // No git data exists yet - add to purgatory 79 .iter()
80 .any(|e| e.event.id.eq(&event.id))
81 {
75 tracing::debug!( 82 tracing::debug!(
76 "No git data found for identifier {}, adding state event {} to purgatory", 83 "processed state event duplicate (already in purgatory): {}",
77 state.identifier, 84 event.id,
78 event.id.to_hex()
79 ); 85 );
80 self.ctx 86 return Ok(WritePolicyResult::Reject {
81 .purgatory 87 status: true, // Client sees OK
82 .add_state(event.clone(), state.identifier.clone(), event.pubkey); 88 message: "duplicate: in purgatory".into(),
83 // Return 0 repos aligned, but this is not an error 89 });
84 return Ok(0); 90 }
91 // get all repositories and state events from db with identifier
92 let db_repo_data = fetch_repository_data(&self.ctx.database, &state.identifier).await?;
93
94 // duplicate check in db
95 if db_repo_data.states.iter().any(|e| e.event.id.eq(&event.id)) {
96 tracing::debug!("processed state event duplicate (in db): {}", event.id,);
97 return Ok(WritePolicyResult::Reject {
98 status: true, // Client sees OK
99 message: "duplicate".into(),
100 });
85 } 101 }
86 102
87 // Identify owner repositories for which this is the latest authorized state 103 // check if git data is avialable
88 let owner_repos = self.identify_owner_repositories(&state).await?; 104 if let Some(repo_with_git_data) =
89 let repo_count = owner_repos.len(); 105 find_repo_with_git_data(&db_repo_data.announcements, &state, &self.ctx.git_data_path)
90 let mut total_aligned = 0; 106 {
91 107 tracing::debug!(
92 // Align each owner repository with the authorized state 108 "processing state event git as data already available: {}",
93 for (_announcement, repo_path) in owner_repos { 109 event.id,
94 let result = self.align_repository_with_state(&repo_path, &state); 110 );
95 111 // find repos for which this state is authorised and align the git refs to this state
96 if result.has_changes() { 112 let by_owner = collect_authorized_maintainers(&db_repo_data.announcements);
97 tracing::info!( 113 let mut repo_count = 0;
98 "Aligned {} with state: created={}, updated={}, deleted={}, head_set={}", 114 for (owner, maintainers) in by_owner {
99 repo_path.display(), 115 if maintainers.contains(&event.pubkey.to_string()) {
100 result.refs_created, 116 if let Some(previous_state) = db_repo_data
101 result.refs_updated, 117 .states
102 result.refs_deleted, 118 .iter()
103 result.head_set 119 .filter(|e| maintainers.contains(&e.event.pubkey.to_string()))
104 ); 120 .max_by_key(|e| e.event.created_at)
105 total_aligned += 1; 121 {
122 // TODO in event of a tie the event with the biggest event id wins
123 if state.event.created_at > previous_state.event.created_at {
124 if let Some(annoucement) = db_repo_data
125 .announcements
126 .iter()
127 .find(|a| a.event.pubkey.to_string().eq(&owner))
128 {
129 let repo_path =
130 self.ctx.git_data_path.join(annoucement.repo_path().clone());
131 // TODO - if repo_path != repo_with_git_data, pass as a datasource for missing data?
132 let result = self.align_repository_with_state(&repo_path, &state);
133 repo_count += 1;
134 tracing::info!(
135 "Aligned {} with state: created={}, updated={}, deleted={}, head_set={}",
136 repo_path.display(),
137 result.refs_created,
138 result.refs_updated,
139 result.refs_deleted,
140 result.head_set
141 );
142 }
143 }
144 }
145 }
106 } 146 }
107 }
108 147
109 if repo_count > 0 {
110 tracing::info!( 148 tracing::info!(
111 "Processed state event for {} repo(s) ({} aligned) with identifier {}", 149 "immediately accepting state event. Was latest authorised state and git data updated for {repo_count} repositories: eventid: {}",
112 repo_count, 150 state.event.id,
113 total_aligned,
114 state.identifier
115 ); 151 );
152 // immediately accept the event, bypassing purgatory
153 Ok(WritePolicyResult::Accept) // event should be saved and broadcast
116 } else { 154 } else {
117 tracing::debug!( 155 // if no git data - add to purgatory
118 "No owner repos to align for state - git data exists but author not authorized or not latest" 156 self.ctx
157 .purgatory
158 .add_state(event.clone(), state.identifier.clone(), event.pubkey);
159 tracing::info!(
160 "state event added to purgatory: eventid: {}, identifier: {}",
161 state.event.id,
162 state.identifier,
119 ); 163 );
164 Ok(WritePolicyResult::Reject {
165 status: true, // Client sees OK
166 message: "purgatory: won't be served until git data arrives".into(),
167 })
120 } 168 }
121
122 Ok(total_aligned)
123 } 169 }
124 170
125 /// Check if any git repositories exist for the given identifier 171 /// Check if any git repositories exist for the given identifier
@@ -473,3 +519,27 @@ impl StatePolicy {
473 result 519 result
474 } 520 }
475} 521}
522
523fn find_repo_with_git_data(
524 announcements: &[RepositoryAnnouncement],
525 state: &RepositoryState,
526 git_data_path: &Path,
527) -> Option<PathBuf> {
528 for announcement in announcements {
529 let repo_path = git_data_path.join(announcement.repo_path().clone());
530 if state.branches.iter().all(|branch_state| {
531 if branch_state.commit.starts_with("ref: ") {
532 true // ignore symlinks
533 } else {
534 git::oid_exists(&repo_path, &branch_state.commit)
535 }
536 }) && state
537 .tags
538 .iter()
539 .all(|tag_state| git::oid_exists(&repo_path, &tag_state.commit))
540 {
541 return Some(repo_path);
542 }
543 }
544 None
545}