From d8d6736d559f379f85940952d6ce605671fc8f68 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 26 Sep 2024 13:45:01 -0400 Subject: [PATCH] Caches OTS web calls to avoid pinging the server repeatedly for the same event. --- .../amethyst/model/LocalCache.kt | 7 +- .../service/ots/OkHttpBlockstreamExplorer.kt | 17 +++++ .../quartz/benchmark/CacheBenchmark.kt | 2 +- .../vitorpamplona/quartz/events/OtsEvent.kt | 69 +++++++++++++++---- 4 files changed, 77 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index d70fff5cd..dd32d0ecf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -116,6 +116,7 @@ import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteModificationEvent import com.vitorpamplona.quartz.events.TorrentCommentEvent import com.vitorpamplona.quartz.events.TorrentEvent +import com.vitorpamplona.quartz.events.VerificationState import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.WikiNoteEvent @@ -1093,7 +1094,9 @@ object LocalCache { if (version.event?.id() == event.id()) return // makes sure the OTS has a valid certificate - if (event.cacheVerify() == null) return // no valid OTS + val verif = event.cacheVerify() + Log.d("AABBCC", "" + verif) + if (verif is VerificationState.Error) return // no valid OTS if (version.event == null) { version.loadEvent(event, author, emptyList()) @@ -2155,7 +2158,7 @@ object LocalCache { notes.forEach { _, item -> val noteEvent = item.event if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) { - noteEvent.verifiedTime?.let { stampedTime -> + (noteEvent.cacheVerify() as? VerificationState.Verified)?.verifiedTime?.let { stampedTime -> if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) { minTime = stampedTime } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt index 1ff4ce4da..4e09ab747 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.ots import android.util.Log +import android.util.LruCache import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.ammolite.service.HttpClientManager @@ -32,6 +33,9 @@ import okhttp3.Request class OkHttpBlockstreamExplorer( val forceProxy: (String) -> Boolean, ) : BitcoinExplorer { + private val cacheHeaders = LruCache(100) + private val cacheHeights = LruCache(100) + /** * Retrieve the block information from the block hash. * @@ -40,6 +44,10 @@ class OkHttpBlockstreamExplorer( * @throws Exception desc */ override fun block(hash: String): BlockHeader { + cacheHeaders.get(hash)?.let { + return it + } + val url = "$BLOCKSTREAM_API_URL/block/$hash" val client = HttpClientManager.getHttpClient(forceProxy(url)) @@ -61,6 +69,9 @@ class OkHttpBlockstreamExplorer( blockHeader.setTime(jsonObject["timestamp"].asInt().toString()) blockHeader.blockHash = hash Log.d("OkHttpBlockstreamExplorer", "$BLOCKSTREAM_API_URL/block/$hash") + + cacheHeaders.put(hash, blockHeader) + return blockHeader } else { throw UrlException("Couldn't open $url: " + it.message + " " + it.code) @@ -77,6 +88,10 @@ class OkHttpBlockstreamExplorer( */ @Throws(Exception::class) override fun blockHash(height: Int): String { + cacheHeights[height]?.let { + return it + } + val url = "$BLOCKSTREAM_API_URL/block-height/$height" val client = HttpClientManager.getHttpClient(forceProxy(url)) @@ -93,6 +108,8 @@ class OkHttpBlockstreamExplorer( val blockHash = it.body.string() Log.d("OkHttpBlockstreamExplorer", "$url $blockHash") + + cacheHeights.put(height, blockHash) return blockHash } else { throw UrlException("Couldn't open $url: " + it.message + " " + it.code) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt index 06b73d84a..a72b0f084 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt @@ -106,7 +106,7 @@ open class BaseCacheBenchmark { } @RunWith(AndroidJUnit4::class) -class CacheLoadingBenchmark : BaseCacheBenchmark() { +class CacheNotStartedBenchmark : BaseCacheBenchmark() { @get:Rule val benchmarkRule = BenchmarkRule() @Test diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt index cefea7c98..bcb715ae8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.events import android.util.Log import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.ots.BlockstreamExplorer @@ -30,6 +31,7 @@ import com.vitorpamplona.quartz.ots.DetachedTimestampFile import com.vitorpamplona.quartz.ots.Hash import com.vitorpamplona.quartz.ots.OpenTimestamps import com.vitorpamplona.quartz.ots.VerifyResult +import com.vitorpamplona.quartz.ots.exceptions.UrlException import com.vitorpamplona.quartz.ots.op.OpSHA256 import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils @@ -37,6 +39,25 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes import kotlinx.coroutines.CancellationException import java.util.Base64 +@Immutable +sealed class VerificationState { + @Immutable object NotStarted : VerificationState() + + @Stable + class Verified( + val verifiedTime: Long, + ) : VerificationState() + + @Immutable class Error( + val errorMessage: String, + ) : VerificationState() + + @Immutable class NetworkError( + val errorMessage: String, + val time: Long = TimeUtils.now(), + ) : VerificationState() +} + @Immutable class OtsEvent( id: HexKey, @@ -47,7 +68,7 @@ class OtsEvent( sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { @Transient - var verifiedTime: Long? = null + var verification: VerificationState = VerificationState.NotStarted override fun countMemory(): Long = super.countMemory() + @@ -61,15 +82,24 @@ class OtsEvent( fun otsByteArray(): ByteArray = Base64.getDecoder().decode(content) - fun cacheVerify(): Long? = - if (verifiedTime != null) { - verifiedTime - } else { - verifiedTime = verify() - verifiedTime + fun cacheVerify(): VerificationState = + when (val verif = verification) { + is VerificationState.Verified -> verif + is VerificationState.NotStarted -> verifyState().also { verification = it } + is VerificationState.NetworkError -> { + // try again in 5 mins + if (verif.time < TimeUtils.fiveMinutesAgo()) { + verifyState().also { verification = it } + } else { + verif + } + } + is VerificationState.Error -> verif } - fun verify(): Long? = digestEvent()?.let { OtsEvent.verify(otsByteArray(), it) } + fun verifyState(): VerificationState = digestEvent()?.let { verify(otsByteArray(), it) } ?: VerificationState.Error("Digest Not found") + + fun verify(): Long? = (verifyState() as? VerificationState.Verified)?.verifiedTime fun info(): String { val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) @@ -98,7 +128,7 @@ class OtsEvent( return if (otsInstance.upgrade(detachedOts)) { // if the change is now verifiable. - if (verify(detachedOts, eventId) != null) { + if (verify(detachedOts, eventId) is VerificationState.Verified) { Base64.getEncoder().encodeToString(detachedOts.serialize()) } else { otsFile @@ -111,28 +141,37 @@ class OtsEvent( fun verify( otsFile: String, eventId: HexKey, - ): Long? = verify(Base64.getDecoder().decode(otsFile), eventId) + ): VerificationState = verify(Base64.getDecoder().decode(otsFile), eventId) fun verify( otsFile: ByteArray, eventId: HexKey, - ): Long? = verify(DetachedTimestampFile.deserialize(otsFile), eventId) + ): VerificationState = verify(DetachedTimestampFile.deserialize(otsFile), eventId) fun verify( detachedOts: DetachedTimestampFile, eventId: HexKey, - ): Long? { + ): VerificationState { try { val result = otsInstance.verify(detachedOts, eventId.hexToByteArray()) if (result == null || result.isEmpty()) { - return null + return VerificationState.Error("Verification hashmap is empty") } else { - return result.get(VerifyResult.Chains.BITCOIN)?.timestamp + val time = result.get(VerifyResult.Chains.BITCOIN)?.timestamp + return if (time != null) { + VerificationState.Verified(time) + } else { + VerificationState.Error("Does not include a Bitcoin verification") + } } } catch (e: Exception) { if (e is CancellationException) throw e Log.e("OpenTimeStamps", "Failed to verify", e) - return null + return if (e is UrlException) { + VerificationState.NetworkError(e.message ?: e.cause?.message ?: "Failed to verify") + } else { + VerificationState.Error(e.message ?: e.cause?.message ?: "Failed to verify") + } } }