mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
fix(identity): select lower-ID owner events on timestamp ties
Restoring owner metadata from index relays selected the higher event ID when timestamps matched, contrary to NIP-01 replacement ordering. Use the shared replacement comparator after filtering by owner, kind and valid signature. This affects profiles and relay lists fetched for identity adoption; publication gating and retry policy are unchanged. Validation: the regression failed before the fix and passes for both kinds, including newer events and invalid or unrelated candidates. Focused owner tests, workspace all-target Clippy and formatting pass. Assisted-by: GPT-6
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user