Caches OTS web calls to avoid pinging the server repeatedly for the same event.

This commit is contained in:
Vitor Pamplona
2024-09-26 13:45:01 -04:00
parent d23a859f34
commit d8d6736d55
4 changed files with 77 additions and 18 deletions

View File

@@ -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
}

View File

@@ -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<String, BlockHeader>(100)
private val cacheHeights = LruCache<Int, String>(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)

View File

@@ -106,7 +106,7 @@ open class BaseCacheBenchmark {
}
@RunWith(AndroidJUnit4::class)
class CacheLoadingBenchmark : BaseCacheBenchmark() {
class CacheNotStartedBenchmark : BaseCacheBenchmark() {
@get:Rule val benchmarkRule = BenchmarkRule()
@Test

View File

@@ -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")
}
}
}