From fef1d15f12737f630101c747cfd4591ed6978796 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 22:42:51 +0000 Subject: [PATCH] fix(ots): never persist the non-terminal Verifying sentinel on the note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verifyOts wrote otsVerification = Verifying before the suspending blockchain verifyState() call and only overwrote it with the real verdict afterwards. That verification runs inside LoadOts's cancellable LaunchedEffect, so a row scrolling off-screen cancels the coroutine at the network suspension point — the verdict write never happens and the note is left stuck at Verifying. cacheVerifyOts treats Verifying as terminal, so earliestOtsVerifiedTime then returns null and the confirmed-timestamp pill silently vanishes for the note's lifetime. The old VerificationStateCache had the same write but its LRU eventually evicted the stuck entry; anchoring the verdict on the long-lived note removed that recovery. Store only terminal verdicts (Verified / Error / NetworkError). A cancelled or in-flight verification leaves the field null and simply retries on the next read; the cost is at worst two concurrent first-time verifications, already benign. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014D5fenZbAhCbDiwv7Rtpvj --- .../nip03Timestamp/NoteOtsVerification.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/NoteOtsVerification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/NoteOtsVerification.kt index 58ed3dd167..cb7e3218d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/NoteOtsVerification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/NoteOtsVerification.kt @@ -49,22 +49,28 @@ fun Note.justOtsVerification(): VerificationState? = otsVerification suspend fun Note.cacheVerifyOts(resolver: OtsResolver): VerificationState { val event = event as? OtsEvent ?: return VerificationState.Error("Not an OTS event") return when (val current = otsVerification) { - is VerificationState.Verifying -> current is VerificationState.Verified -> current + is VerificationState.Error -> current is VerificationState.NetworkError -> if (current.time < TimeUtils.fiveMinutesAgo()) verifyOts(event, resolver) else current - is VerificationState.Error -> current + // null, or a leftover non-terminal state from an interrupted run: (re)verify. else -> verifyOts(event, resolver) } } +/** + * Verifies the attestation and stores ONLY the terminal verdict. We deliberately never persist a + * `Verifying` sentinel: this runs inside a cancellable `LoadOts` LaunchedEffect, so if the coroutine + * were cancelled at the network suspension point after writing `Verifying` but before the verdict, + * that sentinel would stick on the (long-lived) note forever — there is no LRU eviction to recover + * it, unlike the old VerificationStateCache — and the OTS pill would silently vanish. Leaving the + * field untouched until a real verdict lands means a cancelled run simply retries on the next read; + * the cost is at worst two concurrent first-time verifications, which is harmless. + */ private suspend fun Note.verifyOts( event: OtsEvent, resolver: OtsResolver, -): VerificationState { - otsVerification = VerificationState.Verifying - return event.verifyState(resolver).also { otsVerification = it } -} +): VerificationState = event.verifyState(resolver).also { otsVerification = it } /** * The earliest blockchain-verified time (unix seconds) among this note's non-expired OTS