fix(sync): preserve hooks after maintainer pre-save

Cold exact-ID reprocessing collapsed exact-duplicate and replaced save statuses into one terminal duplicate. Maintainer admission intentionally pre-saves newly authorized announcements for membership reconciliation, so that early return skipped the local notification and dependency expansion needed to recover state and Git data.

Preserve the database distinction in the accepted-event persistence outcome. Exact duplicates reached after the initial absence check continue through accepted-event hooks, while genuinely replaced events still return Duplicate without broadcast fan-out. Apply the same distinction to hot-cache promotion hooks.

Correctness assumes an exact duplicate after the pre-admission event-by-ID miss is the intentional policy pre-save or a benign concurrent insertion; Replaced continues to mean a newer coordinate is authoritative. This does not change membership policy, replacement ordering, or fetch scheduling.

Validation: nix develop --command cargo test --locked; focused cold invitation and rejected-maintainer Git-sync integration tests; replaced-sync-event unit regression; cargo fmt --check; git diff --check.
This commit is contained in:
DanConwayDev
2026-08-19 22:24:38 +00:00
parent e9b79db211
commit 3ca2c4babb
4 changed files with 35 additions and 11 deletions
+7
View File
@@ -156,6 +156,13 @@ Performance and Security fixes - along with other improvements; immediate upgrad
### Fixed
- Restore cold rejected-maintainer recovery after policy admission pre-saves
the newly authorized announcement for membership reconciliation. An exact
duplicate from that intentional pre-save now still broadcasts the
announcement and expands its state/Git dependencies, while a genuinely
superseded replaceable event remains classified as a duplicate without
downstream fan-out.
- Discover historical repository roots and descendants from the NIP-65
inboxes of accepted repository owners and maintainers. Public Sync+
instances probe exact repository coordinates and every known root on those
+11 -2
View File
@@ -26,8 +26,14 @@ pub enum SaveContext {
pub enum AcceptedEventSaveOutcome {
/// The database inserted the event.
Saved,
/// The database already had this event or retained a newer replaceable event.
/// The exact event is already stored.
///
/// Write-policy admission may intentionally pre-save an event when
/// subsequent policy work needs to query it. Callers that established the
/// event was absent before admission must still run their post-save hooks.
Duplicate,
/// The database retained a newer replaceable event instead.
Replaced,
}
/// Nostr-owned facade for accepted-event persistence side effects.
@@ -55,9 +61,12 @@ impl<'a> EventPersistence<'a> {
let outcome = match status {
SaveEventStatus::Success => AcceptedEventSaveOutcome::Saved,
SaveEventStatus::Rejected(RejectedReason::Duplicate | RejectedReason::Replaced) => {
SaveEventStatus::Rejected(RejectedReason::Duplicate) => {
AcceptedEventSaveOutcome::Duplicate
}
SaveEventStatus::Rejected(RejectedReason::Replaced) => {
AcceptedEventSaveOutcome::Replaced
}
SaveEventStatus::Rejected(reason) => {
return Err(anyhow::anyhow!(
"Database rejected accepted event {}: {reason:?}",
+11 -6
View File
@@ -88,15 +88,17 @@ impl NostrPurgatoryPromotionHooks {
.save_accepted_event(&state, SaveContext::HotCacheReprocess)
.await
{
Ok(AcceptedEventSaveOutcome::Saved) => {
Ok(
AcceptedEventSaveOutcome::Saved | AcceptedEventSaveOutcome::Duplicate,
) => {
rejected_events_index.remove(&state.id);
relay.notify_event(state.clone());
}
Ok(AcceptedEventSaveOutcome::Duplicate) => {
Ok(AcceptedEventSaveOutcome::Replaced) => {
rejected_events_index.remove(&state.id);
debug!(
event_id = %state.id,
"Re-processed state event was already superseded or stored"
"Re-processed state event was superseded"
);
}
Err(e) => {
@@ -201,7 +203,10 @@ impl PurgatoryPromotionHooks for NostrPurgatoryPromotionHooks {
.save_accepted_event(&hot_event, SaveContext::HotCacheReprocess)
.await
{
Ok(AcceptedEventSaveOutcome::Saved) => {
Ok(
AcceptedEventSaveOutcome::Saved
| AcceptedEventSaveOutcome::Duplicate,
) => {
rejected_events_index.remove(&hot_event.id);
relay.notify_event(hot_event.clone());
self.reprocess_state_dependencies(
@@ -217,7 +222,7 @@ impl PurgatoryPromotionHooks for NostrPurgatoryPromotionHooks {
"Maintainer announcement accepted and saved on re-processing"
);
}
Ok(AcceptedEventSaveOutcome::Duplicate) => {
Ok(AcceptedEventSaveOutcome::Replaced) => {
rejected_events_index.remove(&hot_event.id);
self.reprocess_state_dependencies(
write_policy,
@@ -229,7 +234,7 @@ impl PurgatoryPromotionHooks for NostrPurgatoryPromotionHooks {
.await;
debug!(
event_id = %hot_event.id,
"Re-processed maintainer announcement was already superseded or stored"
"Re-processed maintainer announcement was superseded"
);
}
Err(e) => {
+6 -3
View File
@@ -7568,12 +7568,15 @@ impl SyncManager {
WritePolicyResult::Accept => {
// Save event to database
match write_policy.save_accepted_event(event, save_context).await {
Ok(crate::nostr::persistence::AcceptedEventSaveOutcome::Saved) => {}
Ok(crate::nostr::persistence::AcceptedEventSaveOutcome::Duplicate) => {
Ok(
crate::nostr::persistence::AcceptedEventSaveOutcome::Saved
| crate::nostr::persistence::AcceptedEventSaveOutcome::Duplicate,
) => {}
Ok(crate::nostr::persistence::AcceptedEventSaveOutcome::Replaced) => {
tracing::trace!(
event_id = %event.id,
relay = %relay_url,
"Database retained an existing event during sync"
"Database retained a newer replaceable event during sync"
);
return ProcessResult::Duplicate;
}