upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/sync/metrics.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-09 17:04:06 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-09 17:04:06 +0000
commit5ecd8d6a434f97da94daef2f59166086fbaf5a6b (patch)
tree54c7d3b953a6b1aedd1db6b9a719e18131659df5 /src/sync/metrics.rs
parent895359aeb6746b98ff82944e4fca503f4a6e5439 (diff)
feat: implement state event authorization per GRASP-01 spec
Add comprehensive authorization checks to ensure state events are only accepted from maintainers of accepted repository announcements. This implements the core GRASP-01 requirement that pushes must match the latest state announcement "respecting the maintainer set." Changes: 1. StatePolicy authorization (src/nostr/policy/state.rs): - Check authorization BEFORE git data validation (fail-fast) - Reject if no announcement exists for repository - Reject if author not in maintainer set - Use existing helpers: fetch_repository_data() and pubkey_authorised_for_repo_owners() - Structured logging for all rejections 2. Purgatory invalidation (src/nostr/builder.rs): - New method: check_purgatory_state_events_for_identifier() - Called when announcements accepted (Accept and AcceptMaintainer) - Re-evaluates state events in purgatory for the identifier - Processes newly-authorized events (releases from purgatory) - Keeps unauthorized events for natural expiry (30 min) - Enables retroactive authorization when announcements arrive late 3. Purgatory sync authorization (src/git/sync.rs): - Check authorization BEFORE processing git data - Remove unauthorized events from purgatory (permanent rejection) - Prevents processing even if git data arrives first - Structured logging for monitoring 4. Rejected events tracking (src/sync/rejected_index.rs): - Add support for tracking rejected state events - New methods: add_state(), contains_state() - Separate metrics for state rejections - Enables sync to avoid re-fetching rejected states 5. Sync metrics (src/sync/metrics.rs, src/sync/mod.rs): - Add state-specific metrics (hot cache, cold index) - Track rejected states separately from announcements - Support monitoring of authorization rejections 6. Comprehensive tests (tests/state_authorization.rs): - test_reject_state_without_announcement - test_reject_state_from_unauthorized_author - test_accept_state_from_announcement_author - test_accept_state_from_maintainer Security Impact: - Before: State events could be published by anyone - After: Only maintainers can publish state events - Defense-in-depth: Authorization checked at 3 points: 1. On arrival (StatePolicy) 2. On announcement acceptance (purgatory re-evaluation) 3. On git data arrival (purgatory sync) All tests pass: - 248 unit tests - 51 NIP-34 announcement tests - 4 new state authorization tests - 9 rejected index tests Closes: State authorization requirement from GRASP-01 spec
Diffstat (limited to 'src/sync/metrics.rs')
-rw-r--r--src/sync/metrics.rs103
1 files changed, 103 insertions, 0 deletions
diff --git a/src/sync/metrics.rs b/src/sync/metrics.rs
index a175210..7d6d42d 100644
--- a/src/sync/metrics.rs
+++ b/src/sync/metrics.rs
@@ -56,6 +56,22 @@ pub struct SyncMetrics {
56 rejected_announcements_cold_index_expired_total: IntCounter, 56 rejected_announcements_cold_index_expired_total: IntCounter,
57 /// Total invalidations (maintainer announcements invalidated) 57 /// Total invalidations (maintainer announcements invalidated)
58 rejected_announcements_invalidated_total: IntCounter, 58 rejected_announcements_invalidated_total: IntCounter,
59
60 // === Rejected States Index Metrics ===
61 /// Current number of state events in hot cache
62 rejected_states_hot_cache_current: IntGauge,
63 /// Total hot cache hits (state events re-processed from cache)
64 rejected_states_hot_cache_hits_total: IntCounter,
65 /// Total hot cache misses (state events not in cache)
66 rejected_states_hot_cache_misses_total: IntCounter,
67 /// Total expired state events removed from hot cache
68 rejected_states_hot_cache_expired_total: IntCounter,
69 /// Current number of state event entries in cold index
70 rejected_states_cold_index_current: IntGauge,
71 /// Total state event cold index entries expired and removed
72 rejected_states_cold_index_expired_total: IntCounter,
73 /// Total state event invalidations
74 rejected_states_invalidated_total: IntCounter,
59} 75}
60 76
61impl SyncMetrics { 77impl SyncMetrics {
@@ -172,6 +188,49 @@ impl SyncMetrics {
172 ))?; 188 ))?;
173 registry.register(Box::new(rejected_announcements_invalidated_total.clone()))?; 189 registry.register(Box::new(rejected_announcements_invalidated_total.clone()))?;
174 190
191 // Rejected states metrics
192 let rejected_states_hot_cache_current = IntGauge::with_opts(Opts::new(
193 "ngit_sync_rejected_states_hot_cache_current",
194 "Current number of state events in hot cache (full events, 2 min expiry)",
195 ))?;
196 registry.register(Box::new(rejected_states_hot_cache_current.clone()))?;
197
198 let rejected_states_hot_cache_hits_total = IntCounter::with_opts(Opts::new(
199 "ngit_sync_rejected_states_hot_cache_hits_total",
200 "Total hot cache hits (state events re-processed from cache)",
201 ))?;
202 registry.register(Box::new(rejected_states_hot_cache_hits_total.clone()))?;
203
204 let rejected_states_hot_cache_misses_total = IntCounter::with_opts(Opts::new(
205 "ngit_sync_rejected_states_hot_cache_misses_total",
206 "Total hot cache misses (state events not in cache when invalidated)",
207 ))?;
208 registry.register(Box::new(rejected_states_hot_cache_misses_total.clone()))?;
209
210 let rejected_states_hot_cache_expired_total = IntCounter::with_opts(Opts::new(
211 "ngit_sync_rejected_states_hot_cache_expired_total",
212 "Total expired state events removed from hot cache",
213 ))?;
214 registry.register(Box::new(rejected_states_hot_cache_expired_total.clone()))?;
215
216 let rejected_states_cold_index_current = IntGauge::with_opts(Opts::new(
217 "ngit_sync_rejected_states_cold_index_current",
218 "Current number of state event entries in cold index (metadata only, 7 day expiry)",
219 ))?;
220 registry.register(Box::new(rejected_states_cold_index_current.clone()))?;
221
222 let rejected_states_cold_index_expired_total = IntCounter::with_opts(Opts::new(
223 "ngit_sync_rejected_states_cold_index_expired_total",
224 "Total state event cold index entries expired and removed",
225 ))?;
226 registry.register(Box::new(rejected_states_cold_index_expired_total.clone()))?;
227
228 let rejected_states_invalidated_total = IntCounter::with_opts(Opts::new(
229 "ngit_sync_rejected_states_invalidated_total",
230 "Total state event invalidations (when announcements accepted)",
231 ))?;
232 registry.register(Box::new(rejected_states_invalidated_total.clone()))?;
233
175 Ok(Self { 234 Ok(Self {
176 relay_connected, 235 relay_connected,
177 connection_attempts_total, 236 connection_attempts_total,
@@ -188,6 +247,13 @@ impl SyncMetrics {
188 rejected_announcements_cold_index_current, 247 rejected_announcements_cold_index_current,
189 rejected_announcements_cold_index_expired_total, 248 rejected_announcements_cold_index_expired_total,
190 rejected_announcements_invalidated_total, 249 rejected_announcements_invalidated_total,
250 rejected_states_hot_cache_current,
251 rejected_states_hot_cache_hits_total,
252 rejected_states_hot_cache_misses_total,
253 rejected_states_hot_cache_expired_total,
254 rejected_states_cold_index_current,
255 rejected_states_cold_index_expired_total,
256 rejected_states_invalidated_total,
191 }) 257 })
192 } 258 }
193 259
@@ -396,6 +462,43 @@ impl SyncMetrics {
396 pub fn record_invalidation(&self, count: usize) { 462 pub fn record_invalidation(&self, count: usize) {
397 self.rejected_announcements_invalidated_total.inc_by(count as u64); 463 self.rejected_announcements_invalidated_total.inc_by(count as u64);
398 } 464 }
465
466 // === Rejected States Recording Methods ===
467
468 /// Update state events hot cache current size gauge.
469 pub fn update_states_hot_cache_size(&self, size: usize) {
470 self.rejected_states_hot_cache_current.set(size as i64);
471 }
472
473 /// Record state event hot cache hit (event re-processed from cache).
474 pub fn record_states_hot_cache_hit(&self) {
475 self.rejected_states_hot_cache_hits_total.inc();
476 }
477
478 /// Record state event hot cache miss (event not in cache when invalidated).
479 pub fn record_states_hot_cache_miss(&self) {
480 self.rejected_states_hot_cache_misses_total.inc();
481 }
482
483 /// Record state event hot cache expired entries.
484 pub fn record_states_hot_cache_expired(&self, count: usize) {
485 self.rejected_states_hot_cache_expired_total.inc_by(count as u64);
486 }
487
488 /// Update state events cold index current size gauge.
489 pub fn update_states_cold_index_size(&self, size: usize) {
490 self.rejected_states_cold_index_current.set(size as i64);
491 }
492
493 /// Record state event cold index expired entries.
494 pub fn record_states_cold_index_expired(&self, count: usize) {
495 self.rejected_states_cold_index_expired_total.inc_by(count as u64);
496 }
497
498 /// Record state event invalidation.
499 pub fn record_states_invalidation(&self, count: usize) {
500 self.rejected_states_invalidated_total.inc_by(count as u64);
501 }
399} 502}
400 503
401#[cfg(test)] 504#[cfg(test)]