mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-27 21:16:23 +02:00
Caches OTS web calls to avoid pinging the server repeatedly for the same event.
This commit is contained in:
@@ -116,6 +116,7 @@ import com.vitorpamplona.quartz.events.TextNoteEvent
|
|||||||
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
|
||||||
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||||
|
import com.vitorpamplona.quartz.events.VerificationState
|
||||||
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
|
||||||
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
import com.vitorpamplona.quartz.events.VideoVerticalEvent
|
||||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||||
@@ -1093,7 +1094,9 @@ object LocalCache {
|
|||||||
if (version.event?.id() == event.id()) return
|
if (version.event?.id() == event.id()) return
|
||||||
|
|
||||||
// makes sure the OTS has a valid certificate
|
// 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) {
|
if (version.event == null) {
|
||||||
version.loadEvent(event, author, emptyList())
|
version.loadEvent(event, author, emptyList())
|
||||||
@@ -2155,7 +2158,7 @@ object LocalCache {
|
|||||||
notes.forEach { _, item ->
|
notes.forEach { _, item ->
|
||||||
val noteEvent = item.event
|
val noteEvent = item.event
|
||||||
if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) {
|
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)) {
|
if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) {
|
||||||
minTime = stampedTime
|
minTime = stampedTime
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.service.ots
|
package com.vitorpamplona.amethyst.service.ots
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.LruCache
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||||
@@ -32,6 +33,9 @@ import okhttp3.Request
|
|||||||
class OkHttpBlockstreamExplorer(
|
class OkHttpBlockstreamExplorer(
|
||||||
val forceProxy: (String) -> Boolean,
|
val forceProxy: (String) -> Boolean,
|
||||||
) : BitcoinExplorer {
|
) : BitcoinExplorer {
|
||||||
|
private val cacheHeaders = LruCache<String, BlockHeader>(100)
|
||||||
|
private val cacheHeights = LruCache<Int, String>(100)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the block information from the block hash.
|
* Retrieve the block information from the block hash.
|
||||||
*
|
*
|
||||||
@@ -40,6 +44,10 @@ class OkHttpBlockstreamExplorer(
|
|||||||
* @throws Exception desc
|
* @throws Exception desc
|
||||||
*/
|
*/
|
||||||
override fun block(hash: String): BlockHeader {
|
override fun block(hash: String): BlockHeader {
|
||||||
|
cacheHeaders.get(hash)?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
val url = "$BLOCKSTREAM_API_URL/block/$hash"
|
val url = "$BLOCKSTREAM_API_URL/block/$hash"
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(url))
|
val client = HttpClientManager.getHttpClient(forceProxy(url))
|
||||||
|
|
||||||
@@ -61,6 +69,9 @@ class OkHttpBlockstreamExplorer(
|
|||||||
blockHeader.setTime(jsonObject["timestamp"].asInt().toString())
|
blockHeader.setTime(jsonObject["timestamp"].asInt().toString())
|
||||||
blockHeader.blockHash = hash
|
blockHeader.blockHash = hash
|
||||||
Log.d("OkHttpBlockstreamExplorer", "$BLOCKSTREAM_API_URL/block/$hash")
|
Log.d("OkHttpBlockstreamExplorer", "$BLOCKSTREAM_API_URL/block/$hash")
|
||||||
|
|
||||||
|
cacheHeaders.put(hash, blockHeader)
|
||||||
|
|
||||||
return blockHeader
|
return blockHeader
|
||||||
} else {
|
} else {
|
||||||
throw UrlException("Couldn't open $url: " + it.message + " " + it.code)
|
throw UrlException("Couldn't open $url: " + it.message + " " + it.code)
|
||||||
@@ -77,6 +88,10 @@ class OkHttpBlockstreamExplorer(
|
|||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun blockHash(height: Int): String {
|
override fun blockHash(height: Int): String {
|
||||||
|
cacheHeights[height]?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
val url = "$BLOCKSTREAM_API_URL/block-height/$height"
|
val url = "$BLOCKSTREAM_API_URL/block-height/$height"
|
||||||
val client = HttpClientManager.getHttpClient(forceProxy(url))
|
val client = HttpClientManager.getHttpClient(forceProxy(url))
|
||||||
|
|
||||||
@@ -93,6 +108,8 @@ class OkHttpBlockstreamExplorer(
|
|||||||
val blockHash = it.body.string()
|
val blockHash = it.body.string()
|
||||||
|
|
||||||
Log.d("OkHttpBlockstreamExplorer", "$url $blockHash")
|
Log.d("OkHttpBlockstreamExplorer", "$url $blockHash")
|
||||||
|
|
||||||
|
cacheHeights.put(height, blockHash)
|
||||||
return blockHash
|
return blockHash
|
||||||
} else {
|
} else {
|
||||||
throw UrlException("Couldn't open $url: " + it.message + " " + it.code)
|
throw UrlException("Couldn't open $url: " + it.message + " " + it.code)
|
||||||
|
@@ -106,7 +106,7 @@ open class BaseCacheBenchmark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class CacheLoadingBenchmark : BaseCacheBenchmark() {
|
class CacheNotStartedBenchmark : BaseCacheBenchmark() {
|
||||||
@get:Rule val benchmarkRule = BenchmarkRule()
|
@get:Rule val benchmarkRule = BenchmarkRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.events
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.quartz.encoders.HexKey
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||||
import com.vitorpamplona.quartz.ots.BlockstreamExplorer
|
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.Hash
|
||||||
import com.vitorpamplona.quartz.ots.OpenTimestamps
|
import com.vitorpamplona.quartz.ots.OpenTimestamps
|
||||||
import com.vitorpamplona.quartz.ots.VerifyResult
|
import com.vitorpamplona.quartz.ots.VerifyResult
|
||||||
|
import com.vitorpamplona.quartz.ots.exceptions.UrlException
|
||||||
import com.vitorpamplona.quartz.ots.op.OpSHA256
|
import com.vitorpamplona.quartz.ots.op.OpSHA256
|
||||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
@@ -37,6 +39,25 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
|||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import java.util.Base64
|
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
|
@Immutable
|
||||||
class OtsEvent(
|
class OtsEvent(
|
||||||
id: HexKey,
|
id: HexKey,
|
||||||
@@ -47,7 +68,7 @@ class OtsEvent(
|
|||||||
sig: HexKey,
|
sig: HexKey,
|
||||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
@Transient
|
@Transient
|
||||||
var verifiedTime: Long? = null
|
var verification: VerificationState = VerificationState.NotStarted
|
||||||
|
|
||||||
override fun countMemory(): Long =
|
override fun countMemory(): Long =
|
||||||
super.countMemory() +
|
super.countMemory() +
|
||||||
@@ -61,15 +82,24 @@ class OtsEvent(
|
|||||||
|
|
||||||
fun otsByteArray(): ByteArray = Base64.getDecoder().decode(content)
|
fun otsByteArray(): ByteArray = Base64.getDecoder().decode(content)
|
||||||
|
|
||||||
fun cacheVerify(): Long? =
|
fun cacheVerify(): VerificationState =
|
||||||
if (verifiedTime != null) {
|
when (val verif = verification) {
|
||||||
verifiedTime
|
is VerificationState.Verified -> verif
|
||||||
} else {
|
is VerificationState.NotStarted -> verifyState().also { verification = it }
|
||||||
verifiedTime = verify()
|
is VerificationState.NetworkError -> {
|
||||||
verifiedTime
|
// 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 {
|
fun info(): String {
|
||||||
val detachedOts = DetachedTimestampFile.deserialize(otsByteArray())
|
val detachedOts = DetachedTimestampFile.deserialize(otsByteArray())
|
||||||
@@ -98,7 +128,7 @@ class OtsEvent(
|
|||||||
|
|
||||||
return if (otsInstance.upgrade(detachedOts)) {
|
return if (otsInstance.upgrade(detachedOts)) {
|
||||||
// if the change is now verifiable.
|
// if the change is now verifiable.
|
||||||
if (verify(detachedOts, eventId) != null) {
|
if (verify(detachedOts, eventId) is VerificationState.Verified) {
|
||||||
Base64.getEncoder().encodeToString(detachedOts.serialize())
|
Base64.getEncoder().encodeToString(detachedOts.serialize())
|
||||||
} else {
|
} else {
|
||||||
otsFile
|
otsFile
|
||||||
@@ -111,28 +141,37 @@ class OtsEvent(
|
|||||||
fun verify(
|
fun verify(
|
||||||
otsFile: String,
|
otsFile: String,
|
||||||
eventId: HexKey,
|
eventId: HexKey,
|
||||||
): Long? = verify(Base64.getDecoder().decode(otsFile), eventId)
|
): VerificationState = verify(Base64.getDecoder().decode(otsFile), eventId)
|
||||||
|
|
||||||
fun verify(
|
fun verify(
|
||||||
otsFile: ByteArray,
|
otsFile: ByteArray,
|
||||||
eventId: HexKey,
|
eventId: HexKey,
|
||||||
): Long? = verify(DetachedTimestampFile.deserialize(otsFile), eventId)
|
): VerificationState = verify(DetachedTimestampFile.deserialize(otsFile), eventId)
|
||||||
|
|
||||||
fun verify(
|
fun verify(
|
||||||
detachedOts: DetachedTimestampFile,
|
detachedOts: DetachedTimestampFile,
|
||||||
eventId: HexKey,
|
eventId: HexKey,
|
||||||
): Long? {
|
): VerificationState {
|
||||||
try {
|
try {
|
||||||
val result = otsInstance.verify(detachedOts, eventId.hexToByteArray())
|
val result = otsInstance.verify(detachedOts, eventId.hexToByteArray())
|
||||||
if (result == null || result.isEmpty()) {
|
if (result == null || result.isEmpty()) {
|
||||||
return null
|
return VerificationState.Error("Verification hashmap is empty")
|
||||||
} else {
|
} 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) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
Log.e("OpenTimeStamps", "Failed to verify", 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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user