diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a352e4..ea28d74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Restore relay-owner profiles and relay lists using the lower event ID when + fetched versions share a timestamp. + - Honor same-second lower-ID replacements when de-listing served repositories, capturing superseded history, and choosing rollback or post-deletion state. History capture time no longer overrides the original events' ID tie-break. diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md index c70bc9d..8d6abf8 100644 --- a/docs/explanation/architecture.md +++ b/docs/explanation/architecture.md @@ -91,6 +91,8 @@ runtime: or generated, is sent before at least one user-index relay has been checked for its kind, every send is preceded by a per-relay re-check, and an identity found on an index is adopted locally instead of overwritten. + Among verified owner events of a kind, the newest timestamp wins; equal + timestamps select the lowest event ID. Transient failures retry with a capped backoff, and in private mode the identity stays local so the relay's existence is never advertised - Atomically checkpoint purgatory and rejected-event recovery state every 60 diff --git a/src/nostr/relay_identity.rs b/src/nostr/relay_identity.rs index ac3be6f..6c5ee1a 100644 --- a/src/nostr/relay_identity.rs +++ b/src/nostr/relay_identity.rs @@ -605,11 +605,7 @@ fn newest_owner_event_of_kind( events .iter() .filter(|event| event.pubkey == owner && event.kind == kind && event.verify().is_ok()) - .max_by(|a, b| { - a.created_at - .cmp(&b.created_at) - .then_with(|| a.id.cmp(&b.id)) - }) + .max_by(|a, b| crate::nostr::events::compare_replacement_events(a, b)) .cloned() } @@ -666,6 +662,46 @@ mod tests { use super::*; use nostr::nips::nip65; + #[test] + fn owner_identity_selection_uses_nip01_ordering() { + let owner = Keys::generate(); + let stranger = Keys::generate(); + for kind in [Kind::Metadata, Kind::RelayList] { + let build = |keys: &Keys, kind, timestamp, content: &str| { + EventBuilder::new(kind, content) + .custom_created_at(Timestamp::from_secs(timestamp)) + .finalize(keys) + .unwrap() + }; + let mut pair = [ + build(&owner, kind, 1000, "first"), + build(&owner, kind, 1000, "second"), + ]; + pair.sort_by_key(|event| event.id); + let [winner, loser] = pair; + let mut invalid = build(&owner, kind, 2000, "invalid"); + invalid.content.push_str("tampered"); + let mut events = std::collections::BTreeSet::from([ + winner.clone(), + loser, + build(&stranger, kind, 2000, "wrong author"), + build(&owner, Kind::TextNote, 2000, "wrong kind"), + invalid, + build(&owner, kind, 999, "older"), + ]); + assert_eq!( + newest_owner_event_of_kind(&events, owner.public_key(), kind), + Some(winner) + ); + let newer = build(&owner, kind, 1001, "newer"); + events.insert(newer.clone()); + assert_eq!( + newest_owner_event_of_kind(&events, owner.public_key(), kind), + Some(newer) + ); + } + } + #[test] fn identity_is_minimal_and_lists_only_this_relay_for_read_and_write() { let config = Config::for_testing();