diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt index 59d00a98e..e1000ea74 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache import com.vitorpamplona.amethyst.commons.services.nwc.NwcPaymentTracker import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.checkSignature @@ -52,6 +53,7 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.BufferOverflow @@ -165,19 +167,12 @@ class DesktopLocalCache : ICacheProvider { } } - // ----- Event verification ----- - - /** - * Verifies an event's id-hash and Schnorr signature. On failure, logs - * the offending kind/createdAt and returns false so callers can drop it. - * Mirrors Amethyst Android's [com.vitorpamplona.amethyst.model.LocalCache.justVerify] - * so unverified relay traffic never reaches the desktop cache. - */ fun justVerify(event: Event): Boolean = if (!event.verify()) { try { event.checkSignature() } catch (e: Exception) { + if (e is CancellationException) throw e Log.w("Event Verification Failed") { "Kind: ${event.kind} createdAt=${event.createdAt} id=${event.id} pubkey=${event.pubKey} reason=${e.message}" } @@ -189,25 +184,16 @@ class DesktopLocalCache : ICacheProvider { // ----- Event consumption ----- - /** - * Routes an event to the appropriate consume method. - * Returns true if the event was consumed (new), false if already seen - * or if signature verification failed. - */ fun consume( event: Event, relay: NormalizedRelayUrl?, + wasVerified: Boolean = false, ): Boolean { - if (!justVerify(event)) return false - return consumeAssumingVerified(event, relay) + if (!wasVerified && !justVerify(event)) return false + return route(event, relay) } - /** - * Routes an already-verified event to its consume method. Internal seam - * exposed for tests that exercise routing/cache logic with synthetic - * events whose signatures are not real. - */ - internal fun consumeAssumingVerified( + private fun route( event: Event, relay: NormalizedRelayUrl?, ): Boolean = @@ -493,8 +479,9 @@ class DesktopLocalCache : ICacheProvider { zappedNote: Note?, relay: NormalizedRelayUrl?, onResponse: suspend (LnZapPaymentResponseEvent) -> Unit, + wasVerified: Boolean = false, ): Boolean { - if (!justVerify(event)) return false + if (!wasVerified && !justVerify(event)) return false val note = getOrCreateNote(event.id) val author = getOrCreateUser(event.pubKey) @@ -521,8 +508,9 @@ class DesktopLocalCache : ICacheProvider { fun consume( event: LnZapPaymentResponseEvent, relay: NormalizedRelayUrl?, + wasVerified: Boolean = false, ): Boolean { - if (!justVerify(event)) return false + if (!wasVerified && !justVerify(event)) return false val requestId = event.requestId() val pending = paymentTracker.onResponseReceived(requestId) ?: return false @@ -589,7 +577,7 @@ class DesktopLocalCache : ICacheProvider { if (!justVerify(event)) return false // For addressable/replaceable events, store in the addressable note cache // so state holders (Nip65RelayListState, etc.) pick it up via their flows - if (event is com.vitorpamplona.quartz.nip01Core.core.AddressableEvent) { + if (event is AddressableEvent) { val address = event.address() val note = getOrCreateAddressableNote(address) val author = getOrCreateUser(event.pubKey) ?: return false diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandler.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandler.kt index e53804571..3f34c8626 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandler.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandler.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEv import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response +import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import kotlin.coroutines.resume @@ -134,27 +135,31 @@ class NwcPaymentHandler( filters = listOf(filter), onEvent = { event, relay -> if (event is LnZapPaymentResponseEvent && event.requestId() == requestId) { - // Unsubscribe - relayManager.closeSubscription(nwcConnection.relayUri, subId) + // Move verify + cache mutation + decrypt off the relay's + // WebSocket reader thread; Schnorr verify is non-trivial + // CPU work and shouldn't block frame parsing. + @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class) + kotlinx.coroutines.GlobalScope.launch(kotlinx.coroutines.Dispatchers.IO) { + if (!localCache.justVerify(event)) return@launch - // Store response note and link to zapped note - val responseNote = localCache.getOrCreateNote(event.id) - responseNote.loadEvent(event, localCache.getOrCreateUser(event.pubKey), emptyList()) - responseNote.addRelay(relay) - zappedNote?.addZapPayment(requestNote, responseNote) + relayManager.closeSubscription(nwcConnection.relayUri, subId) - // Decrypt and process response - try { - kotlinx.coroutines.runBlocking { + val responseNote = localCache.getOrCreateNote(event.id) + responseNote.loadEvent(event, localCache.getOrCreateUser(event.pubKey), emptyList()) + responseNote.addRelay(relay) + zappedNote?.addZapPayment(requestNote, responseNote) + + try { val response = event.decrypt(nwcSigner) val result = processResponse(response) if (continuation.isActive) { continuation.resume(result) } - } - } catch (e: Exception) { - if (continuation.isActive) { - continuation.resume(PaymentResult.Error("Failed to decrypt response: ${e.message}")) + } catch (e: Exception) { + if (e is kotlinx.coroutines.CancellationException) throw e + if (continuation.isActive) { + continuation.resume(PaymentResult.Error("Failed to decrypt response: ${e.message}")) + } } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt index 254acb19a..2c71229f1 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt @@ -33,6 +33,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -120,46 +122,25 @@ class DesktopRelaySubscriptionsCoordinator( fun consumeEvent( event: Event, relay: NormalizedRelayUrl?, + wasVerified: Boolean = false, ) { scope.launch(Dispatchers.IO) { try { - runConsume(localCache.consume(event, relay), event) + if (!localCache.consume(event, relay, wasVerified)) return@launch + _lastEventAt.value = System.currentTimeMillis() + val note = localCache.getNoteIfExists(event.id) ?: return@launch + eventBundler.invalidateList(note) { batch -> + localCache.eventStream.emitNewNotes(batch) + } } catch (e: Exception) { - println("Coordinator: failed to consume kind=${event.kind} id=${event.id} relay=$relay: ${e.message}") + if (e is CancellationException) throw e + Log.w("DesktopRelaySubscriptionsCoordinator") { + "Failed to consume kind=${event.kind} id=${event.id} relay=$relay: ${e.message}" + } } } } - /** - * Test-only seam — bypasses signature verification in the cache so unit - * tests can pump synthetic events through the full coordinator pipeline. - * Production code MUST use [consumeEvent]. - */ - internal fun consumeEventAssumingVerified( - event: Event, - relay: NormalizedRelayUrl?, - ) { - scope.launch(Dispatchers.IO) { - try { - runConsume(localCache.consumeAssumingVerified(event, relay), event) - } catch (e: Exception) { - println("Coordinator: failed to consume kind=${event.kind} id=${event.id} relay=$relay: ${e.message}") - } - } - } - - private suspend fun runConsume( - consumed: Boolean, - event: Event, - ) { - if (!consumed) return - _lastEventAt.value = System.currentTimeMillis() - val note = localCache.getNoteIfExists(event.id) ?: return - eventBundler.invalidateList(note) { batch -> - localCache.eventStream.emitNewNotes(batch) - } - } - /** * Request a consolidated interaction subscription for the given note IDs. * Subscribes to kinds 7 (reactions), 9735 (zaps), 6 (reposts), and 1 (replies) diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/CoordinatorPipelineTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/CoordinatorPipelineTest.kt index 5f4efd162..9ef4dead6 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/CoordinatorPipelineTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/CoordinatorPipelineTest.kt @@ -53,7 +53,7 @@ import kotlin.test.assertTrue * These tests use a stub INostrClient (no real relay connections) and exercise * the full event consumption path through DesktopRelaySubscriptionsCoordinator. * - * Key invariant being tested: when coordinator.consumeEventAssumingVerified() is called, + * Key invariant being tested: when coordinator.consumeEvent() is called, * the event should flow through cache → eventStream → ViewModel.feedState. */ class CoordinatorPipelineTest { @@ -167,14 +167,14 @@ class CoordinatorPipelineTest { content = "Hello from relay", sig = dummySig, ) - coordinator.consumeEventAssumingVerified(event, relayUrl) + coordinator.consumeEvent(event, relayUrl, wasVerified = true) waitForBundler() val state = vm.feedState.feedContent.value assertIs( state, - "ViewModel should be Loaded after coordinator.consumeEventAssumingVerified()", + "ViewModel should be Loaded after coordinator.consumeEvent()", ) assertTrue( vm.feedState.visibleNotes().any { it.idHex == event.id }, @@ -203,7 +203,7 @@ class CoordinatorPipelineTest { content = "test", sig = dummySig, ) - coordinator.consumeEventAssumingVerified(event, relayUrl) + coordinator.consumeEvent(event, relayUrl, wasVerified = true) waitForBundler() assertTrue(coordinator.lastEventAt.value != null, "lastEventAt should be set after consumeEvent") @@ -227,7 +227,7 @@ class CoordinatorPipelineTest { content = "", sig = dummySig, ) - coordinator.consumeEventAssumingVerified(contactEvent, relayUrl) + coordinator.consumeEvent(contactEvent, relayUrl, wasVerified = true) waitForBundler() assertTrue( @@ -255,7 +255,7 @@ class CoordinatorPipelineTest { content = "", sig = dummySig, ) - coordinator.consumeEventAssumingVerified(contactEvent, relayUrl) + coordinator.consumeEvent(contactEvent, relayUrl, wasVerified = true) waitForBundler() // Step 2: Create following feed ViewModel @@ -274,7 +274,7 @@ class CoordinatorPipelineTest { content = "Note from followed user", sig = dummySig, ) - coordinator.consumeEventAssumingVerified(textEvent, relayUrl) + coordinator.consumeEvent(textEvent, relayUrl, wasVerified = true) waitForBundler() val state = vm.feedState.feedContent.value @@ -311,7 +311,7 @@ class CoordinatorPipelineTest { content = "Note that won't show", sig = dummySig, ) - coordinator.consumeEventAssumingVerified(textEvent, relayUrl) + coordinator.consumeEvent(textEvent, relayUrl, wasVerified = true) waitForBundler() assertIs( @@ -349,8 +349,8 @@ class CoordinatorPipelineTest { ) // Consume same event twice (can happen with multiple relays) - coordinator.consumeEventAssumingVerified(event, relayUrl) - coordinator.consumeEventAssumingVerified(event, NormalizedRelayUrl("wss://relay2.test/")) + coordinator.consumeEvent(event, relayUrl, wasVerified = true) + coordinator.consumeEvent(event, NormalizedRelayUrl("wss://relay2.test/"), wasVerified = true) waitForBundler() assertTrue( diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt index 1d7bc219f..457a9a1fc 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopCachePipelineTest.kt @@ -127,7 +127,7 @@ class DesktopCachePipelineTest { val cache = DesktopLocalCache() val event = textNote("note1".padEnd(64, '0'), userPubKey) - val consumed = cache.consumeAssumingVerified(event, relayUrl) + val consumed = cache.consume(event, relayUrl, wasVerified = true) assertTrue(consumed, "First consume should return true") val note = cache.getNoteIfExists("note1".padEnd(64, '0')) @@ -140,8 +140,8 @@ class DesktopCachePipelineTest { val cache = DesktopLocalCache() val event = textNote("note1".padEnd(64, '0'), userPubKey) - cache.consumeAssumingVerified(event, relayUrl) - val secondConsume = cache.consumeAssumingVerified(event, relayUrl) + cache.consume(event, relayUrl, wasVerified = true) + val secondConsume = cache.consume(event, relayUrl, wasVerified = true) assertTrue(!secondConsume, "Second consume of same event should return false") } @@ -151,7 +151,7 @@ class DesktopCachePipelineTest { val cache = DesktopLocalCache() val event = contactList("cl1".padEnd(64, '0'), userPubKey, listOf(followedPubKey)) - cache.consumeAssumingVerified(event, relayUrl) + cache.consume(event, relayUrl, wasVerified = true) assertEquals(setOf(followedPubKey), cache.followedUsers.value) } @@ -168,8 +168,8 @@ class DesktopCachePipelineTest { createdAt = 200, ) - cache.consumeAssumingVerified(old, relayUrl) - cache.consumeAssumingVerified(newer, relayUrl) + cache.consume(old, relayUrl, wasVerified = true) + cache.consume(newer, relayUrl, wasVerified = true) assertEquals(setOf(followedPubKey, unfollowedPubKey), cache.followedUsers.value) } @@ -180,8 +180,8 @@ class DesktopCachePipelineTest { val newer = contactList("cl2".padEnd(64, '0'), userPubKey, listOf(followedPubKey, unfollowedPubKey), createdAt = 200) val old = contactList("cl1".padEnd(64, '0'), userPubKey, listOf(followedPubKey), createdAt = 100) - cache.consumeAssumingVerified(newer, relayUrl) - cache.consumeAssumingVerified(old, relayUrl) + cache.consume(newer, relayUrl, wasVerified = true) + cache.consume(old, relayUrl, wasVerified = true) assertEquals( setOf(followedPubKey, unfollowedPubKey), @@ -197,8 +197,8 @@ class DesktopCachePipelineTest { val note = textNote(noteId, userPubKey) val react = reaction("react1".padEnd(64, '0'), followedPubKey, noteId) - cache.consumeAssumingVerified(note, relayUrl) - cache.consumeAssumingVerified(react, relayUrl) + cache.consume(note, relayUrl, wasVerified = true) + cache.consume(react, relayUrl, wasVerified = true) val cachedNote = cache.getNoteIfExists(noteId)!! assertTrue(cachedNote.countReactions() > 0, "Note should have reactions after consuming reaction event") @@ -223,7 +223,7 @@ class DesktopCachePipelineTest { delay(50) val event = textNote("note1".padEnd(64, '0'), userPubKey) - cache.consumeAssumingVerified(event, relayUrl) + cache.consume(event, relayUrl, wasVerified = true) val note = cache.getNoteIfExists(event.id)!! cache.emitNewNotes(setOf(note)) @@ -244,9 +244,9 @@ class DesktopCachePipelineTest { val filter = DesktopGlobalFeedFilter(cache) // Add notes from different authors - cache.consumeAssumingVerified(textNote("n1".padEnd(64, '0'), userPubKey, createdAt = 100), relayUrl) - cache.consumeAssumingVerified(textNote("n2".padEnd(64, '0'), followedPubKey, createdAt = 200), relayUrl) - cache.consumeAssumingVerified(textNote("n3".padEnd(64, '0'), unfollowedPubKey, createdAt = 300), relayUrl) + cache.consume(textNote("n1".padEnd(64, '0'), userPubKey, createdAt = 100), relayUrl, wasVerified = true) + cache.consume(textNote("n2".padEnd(64, '0'), followedPubKey, createdAt = 200), relayUrl, wasVerified = true) + cache.consume(textNote("n3".padEnd(64, '0'), unfollowedPubKey, createdAt = 300), relayUrl, wasVerified = true) val feed = filter.feed() assertEquals(3, feed.size, "Global feed should contain all text notes") @@ -255,10 +255,10 @@ class DesktopCachePipelineTest { @Test fun `FollowingFeedFilter only includes notes from followed users`() { val cache = DesktopLocalCache() - cache.consumeAssumingVerified(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl) + cache.consume(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl, wasVerified = true) - cache.consumeAssumingVerified(textNote("n1".padEnd(64, '0'), followedPubKey, createdAt = 100), relayUrl) - cache.consumeAssumingVerified(textNote("n2".padEnd(64, '0'), unfollowedPubKey, createdAt = 200), relayUrl) + cache.consume(textNote("n1".padEnd(64, '0'), followedPubKey, createdAt = 100), relayUrl, wasVerified = true) + cache.consume(textNote("n2".padEnd(64, '0'), unfollowedPubKey, createdAt = 200), relayUrl, wasVerified = true) val filter = DesktopFollowingFeedFilter(cache) { cache.followedUsers.value } val feed = filter.feed() @@ -270,7 +270,7 @@ class DesktopCachePipelineTest { @Test fun `FollowingFeedFilter returns empty when no follows`() { val cache = DesktopLocalCache() - cache.consumeAssumingVerified(textNote("n1".padEnd(64, '0'), followedPubKey), relayUrl) + cache.consume(textNote("n1".padEnd(64, '0'), followedPubKey), relayUrl, wasVerified = true) val filter = DesktopFollowingFeedFilter(cache) { emptySet() } val feed = filter.feed() @@ -281,8 +281,8 @@ class DesktopCachePipelineTest { @Test fun `ProfileFeedFilter only shows notes from target pubkey`() { val cache = DesktopLocalCache() - cache.consumeAssumingVerified(textNote("n1".padEnd(64, '0'), followedPubKey, createdAt = 100), relayUrl) - cache.consumeAssumingVerified(textNote("n2".padEnd(64, '0'), unfollowedPubKey, createdAt = 200), relayUrl) + cache.consume(textNote("n1".padEnd(64, '0'), followedPubKey, createdAt = 100), relayUrl, wasVerified = true) + cache.consume(textNote("n2".padEnd(64, '0'), unfollowedPubKey, createdAt = 200), relayUrl, wasVerified = true) val filter = DesktopProfileFeedFilter(followedPubKey, cache) val feed = filter.feed() @@ -297,8 +297,8 @@ class DesktopCachePipelineTest { val rootId = "root".padEnd(64, '0') val replyId = "reply".padEnd(64, '0') - cache.consumeAssumingVerified(textNote(rootId, userPubKey, createdAt = 100), relayUrl) - cache.consumeAssumingVerified(textNote(replyId, followedPubKey, createdAt = 200, replyToId = rootId), relayUrl) + cache.consume(textNote(rootId, userPubKey, createdAt = 100), relayUrl, wasVerified = true) + cache.consume(textNote(replyId, followedPubKey, createdAt = 200, replyToId = rootId), relayUrl, wasVerified = true) val filter = DesktopThreadFilter(rootId, cache) val feed = filter.feed() @@ -310,11 +310,11 @@ class DesktopCachePipelineTest { fun `NotificationFeedFilter shows events tagging user`() { val cache = DesktopLocalCache() val noteId = "note1".padEnd(64, '0') - cache.consumeAssumingVerified(textNote(noteId, userPubKey, createdAt = 100), relayUrl) + cache.consume(textNote(noteId, userPubKey, createdAt = 100), relayUrl, wasVerified = true) // Reaction from someone else targeting user's note val react = reaction("react1".padEnd(64, '0'), followedPubKey, noteId, createdAt = 200) - cache.consumeAssumingVerified(react, relayUrl) + cache.consume(react, relayUrl, wasVerified = true) val filter = DesktopNotificationFeedFilter(userPubKey, cache) val feed = filter.feed() @@ -334,7 +334,7 @@ class DesktopCachePipelineTest { fun `ViewModel starts in Loading then transitions to Loaded after refresh`() = runBlocking { val cache = DesktopLocalCache() - cache.consumeAssumingVerified(textNote("n1".padEnd(64, '0'), userPubKey), relayUrl) + cache.consume(textNote("n1".padEnd(64, '0'), userPubKey), relayUrl, wasVerified = true) val vm = DesktopFeedViewModel(DesktopGlobalFeedFilter(cache), cache) @@ -373,7 +373,7 @@ class DesktopCachePipelineTest { // Simulate relay event arriving val event = textNote("n1".padEnd(64, '0'), userPubKey) - cache.consumeAssumingVerified(event, relayUrl) + cache.consume(event, relayUrl, wasVerified = true) val note = cache.getNoteIfExists(event.id)!! cache.emitNewNotes(setOf(note)) @@ -389,7 +389,7 @@ class DesktopCachePipelineTest { fun `Following ViewModel only shows followed users notes via eventStream`() = runBlocking { val cache = DesktopLocalCache() - cache.consumeAssumingVerified(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl) + cache.consume(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl, wasVerified = true) val filter = DesktopFollowingFeedFilter(cache) { cache.followedUsers.value } val vm = DesktopFeedViewModel(filter, cache) @@ -397,7 +397,7 @@ class DesktopCachePipelineTest { // Add followed user's note val e1 = textNote("n1".padEnd(64, '0'), followedPubKey, createdAt = 100) - cache.consumeAssumingVerified(e1, relayUrl) + cache.consume(e1, relayUrl, wasVerified = true) val note1 = cache.getNoteIfExists(e1.id)!! cache.emitNewNotes(setOf(note1)) waitForBundler() @@ -406,7 +406,7 @@ class DesktopCachePipelineTest { // Add unfollowed user's note val e2 = textNote("n2".padEnd(64, '0'), unfollowedPubKey, createdAt = 200) - cache.consumeAssumingVerified(e2, relayUrl) + cache.consume(e2, relayUrl, wasVerified = true) val note2 = cache.getNoteIfExists(e2.id)!! cache.emitNewNotes(setOf(note2)) waitForBundler() @@ -422,7 +422,7 @@ class DesktopCachePipelineTest { // No contact list consumed — followedUsers remains empty val e1 = textNote("n1".padEnd(64, '0'), followedPubKey) - cache.consumeAssumingVerified(e1, relayUrl) + cache.consume(e1, relayUrl, wasVerified = true) val filter = DesktopFollowingFeedFilter(cache) { cache.followedUsers.value } val vm = DesktopFeedViewModel(filter, cache) @@ -442,8 +442,8 @@ class DesktopCachePipelineTest { @Test fun `clear resets all cache state`() { val cache = DesktopLocalCache() - cache.consumeAssumingVerified(textNote("n1".padEnd(64, '0'), userPubKey), relayUrl) - cache.consumeAssumingVerified(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl) + cache.consume(textNote("n1".padEnd(64, '0'), userPubKey), relayUrl, wasVerified = true) + cache.consume(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl, wasVerified = true) cache.clear() @@ -459,9 +459,9 @@ class DesktopCachePipelineTest { @Test fun `global feed is sorted newest first`() { val cache = DesktopLocalCache() - cache.consumeAssumingVerified(textNote("old".padEnd(64, '0'), userPubKey, createdAt = 100), relayUrl) - cache.consumeAssumingVerified(textNote("mid".padEnd(64, '0'), userPubKey, createdAt = 200), relayUrl) - cache.consumeAssumingVerified(textNote("new".padEnd(64, '0'), userPubKey, createdAt = 300), relayUrl) + cache.consume(textNote("old".padEnd(64, '0'), userPubKey, createdAt = 100), relayUrl, wasVerified = true) + cache.consume(textNote("mid".padEnd(64, '0'), userPubKey, createdAt = 200), relayUrl, wasVerified = true) + cache.consume(textNote("new".padEnd(64, '0'), userPubKey, createdAt = 300), relayUrl, wasVerified = true) val filter = DesktopGlobalFeedFilter(cache) val feed = filter.feed() @@ -487,7 +487,7 @@ class DesktopCachePipelineTest { sig = dummySig, ) - cache.consumeAssumingVerified(metadata, relayUrl) + cache.consume(metadata, relayUrl, wasVerified = true) val user = cache.getUserIfExists(userPubKey) assertTrue(user != null, "User should exist after metadata consumption") @@ -506,12 +506,12 @@ class DesktopCachePipelineTest { // Create a text note val textEvent = textNote("t1".padEnd(64, '0'), userPubKey) - cache.consumeAssumingVerified(textEvent, relayUrl) + cache.consume(textEvent, relayUrl, wasVerified = true) val textNote = cache.getNoteIfExists(textEvent.id)!! // Create a reaction (not a text note) val reactEvent = reaction("r1".padEnd(64, '0'), userPubKey, "t1".padEnd(64, '0')) - cache.consumeAssumingVerified(reactEvent, relayUrl) + cache.consume(reactEvent, relayUrl, wasVerified = true) val reactNote = cache.getNoteIfExists(reactEvent.id)!! val filtered = filter.applyFilter(setOf(textNote, reactNote)) @@ -523,16 +523,16 @@ class DesktopCachePipelineTest { @Test fun `FollowingFeedFilter applyFilter respects follow set`() { val cache = DesktopLocalCache() - cache.consumeAssumingVerified(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl) + cache.consume(contactList("cl".padEnd(64, '0'), userPubKey, listOf(followedPubKey)), relayUrl, wasVerified = true) val filter = DesktopFollowingFeedFilter(cache) { cache.followedUsers.value } val e1 = textNote("n1".padEnd(64, '0'), followedPubKey) - cache.consumeAssumingVerified(e1, relayUrl) + cache.consume(e1, relayUrl, wasVerified = true) val note1 = cache.getNoteIfExists(e1.id)!! val e2 = textNote("n2".padEnd(64, '0'), unfollowedPubKey) - cache.consumeAssumingVerified(e2, relayUrl) + cache.consume(e2, relayUrl, wasVerified = true) val note2 = cache.getNoteIfExists(e2.id)!! val filtered = filter.applyFilter(setOf(note1, note2)) @@ -592,7 +592,7 @@ class DesktopCachePipelineTest { sig = dummySig, ) - cache.consumeAssumingVerified(metadata, relayUrl) + cache.consume(metadata, relayUrl, wasVerified = true) val user = cache.getUserIfExists(userPubKey)!! val cached = user.metadataOrNull() diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCacheVerifyTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCacheVerifyTest.kt index dc590a897..3ed2085d9 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCacheVerifyTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCacheVerifyTest.kt @@ -78,7 +78,9 @@ class DesktopLocalCacheVerifyTest { } @Test - fun `consume rejects an event with an all-zero signature`() { + fun `consume rejects an event whose id-hash does not match its content`() { + // Synthetic event with arbitrary id and signature — fails the id check + // before the Schnorr verify is even attempted. val cache = DesktopLocalCache() val event = TextNoteEvent( @@ -128,7 +130,7 @@ class DesktopLocalCacheVerifyTest { } @Test - fun `consumeAssumingVerified bypass still routes test events`() { + fun `wasVerified bypass routes synthetic test events`() { val cache = DesktopLocalCache() val event = TextNoteEvent( @@ -140,9 +142,9 @@ class DesktopLocalCacheVerifyTest { sig = "0".repeat(128), ) - val consumed = cache.consumeAssumingVerified(event, relayUrl) + val consumed = cache.consume(event, relayUrl, wasVerified = true) - assertTrue(consumed, "consumeAssumingVerified must skip the signature check") + assertTrue(consumed, "wasVerified=true must skip the signature check") assertNotNull(cache.getNoteIfExists(event.id)) } }