diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-31 09:17:49 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-31 09:17:49 +0000 |
| commit | 3d6901831904141166d9ed8f47813c45cba109b6 (patch) | |
| tree | 44d0a431b148ad301971fadb7017dfbf937e45ff /src | |
| parent | 08ab20509b9c730d3db98dd6e9deb5e2b548979e (diff) | |
purgatory: fix state event receive code
Diffstat (limited to 'src')
| -rw-r--r-- | src/git/authorization.rs | 1 | ||||
| -rw-r--r-- | src/git/mod.rs | 24 | ||||
| -rw-r--r-- | src/nostr/builder.rs | 47 | ||||
| -rw-r--r-- | src/nostr/policy/state.rs | 168 |
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 | ||
| 85 | pub 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 | |||
| 97 | pub 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 @@ | |||
| 1 | use std::path::{Path, PathBuf}; | ||
| 2 | |||
| 3 | use anyhow::{Context, Result}; | ||
| 4 | use 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 @@ | |||
| 5 | use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag}; | 9 | use nostr_relay_builder::prelude::{Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag}; |
| 6 | 10 | ||
| 7 | use super::PolicyContext; | 11 | use super::PolicyContext; |
| 8 | use crate::git; | 12 | use crate::git::authorization::{collect_authorized_maintainers, fetch_repository_data}; |
| 13 | use crate::git::{self}; | ||
| 9 | use crate::nostr::events::{ | 14 | use 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 | |||
| 523 | fn 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 | } | ||