From 079822ed9d3fb0f5fe281c94ad66f3923ac0b064 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 14:30:40 +0000 Subject: [PATCH] fix: NPE in AntiSpamFilter.isSpam when the first duplicate was evicted from the LRU cache The spam check can fire via the spamMessages record alone, after the original event id/address has been evicted from the 2000-entry recentEventIds/recentAddressables LruCache. In that case existingEvent / existingAddress is null and building the njump link threw a NullPointerException, aborting event consumption in LocalCache. Fall back to the current event's link when the original is no longer cached, and use setOfNotNull in logOffender so a null cache entry can't sneak into the Spammer sets through the platform-type hole. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UGiCujAHWMbmGT89X77S4K --- .../vitorpamplona/amethyst/model/AntiSpamFilter.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 791d69dff1..dbecbb436f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -82,10 +82,12 @@ class AntiSpamFilter { (recentAddressables[hash] != null && recentAddressables[hash] != address) || (spamMessages[hash] != null && !spamMessages[hash].duplicatedEventAddresses.contains(address)) ) { + // may be null if the first duplicate was evicted from the LRU cache + // while the spammer record still matches this hash. val existingAddress = recentAddressables[hash] - val link1 = njumpLink(NAddress.create(existingAddress.kind, existingAddress.pubKeyHex, existingAddress.dTag, relay)) val link2 = njumpLink(NAddress.create(event.kind, event.pubKey, event.dTag(), relay)) + val link1 = existingAddress?.let { njumpLink(NAddress.create(it.kind, it.pubKeyHex, it.dTag, relay)) } ?: link2 Log.w("Duplicated/SPAM") { "${relay?.url} $link1 $link2" } @@ -111,8 +113,10 @@ class AntiSpamFilter { (existingEvent != null && existingEvent != event.id) || (spamMessages[hash] != null && !spamMessages[hash].duplicatedEventIds.contains(event.id)) ) { - val link1 = njumpLink(NEvent.create(existingEvent, null, null, relay)) val link2 = njumpLink(NEvent.create(event.id, null, null, relay)) + // existingEvent may be null if the first duplicate was evicted from the + // LRU cache while the spammer record still matches this hash. + val link1 = existingEvent?.let { njumpLink(NEvent.create(it, null, null, relay)) } ?: link2 Log.w("Duplicated/SPAM") { "${relay?.url} $link1 $link2" } @@ -149,12 +153,12 @@ class AntiSpamFilter { Spammer( pubkeyHex = event.pubKey, duplicatedEventIds = setOf(), - duplicatedEventAddresses = setOf(recentAddressables[hashCode], event.address()), + duplicatedEventAddresses = setOfNotNull(recentAddressables[hashCode], event.address()), ) } else { Spammer( pubkeyHex = event.pubKey, - duplicatedEventIds = setOf(recentEventIds[hashCode], event.id), + duplicatedEventIds = setOfNotNull(recentEventIds[hashCode], event.id), duplicatedEventAddresses = setOf(), ) }