diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt new file mode 100644 index 000000000..7dd5626fe --- /dev/null +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer +import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder +import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.OtsEvent +import com.vitorpamplona.quartz.ots.OpenTimestamps +import com.vitorpamplona.quartz.signers.NostrSignerInternal +import junit.framework.TestCase.assertEquals +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@RunWith(AndroidJUnit4::class) +class OkHttpOtsTest { + val otsEvent = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqCiiW0FlsU9lqK5f1A+cL6CGJ1Ah4V/A1yNJY/stUE3wECJz6ng/QxU5Z6xwaMx97qkI//AQqJv8bEMrGTplGWRv5qm4DgjxIHkcQqzpL0Fjr9VBAAijDe0IsQYpOhw1SIjZIgQa6i16CPEEZck7CvAIxR0AloJzCZoAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEOwPtjIkKI1hmtv9t1kuxZcI8QRlyTsK8Ahl0wrCSggZzgCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEE1dVGa8JCuf2ek0c5ybDKII8SCBoVz8Sal45Kd1O8STWIGJTcl5JPtAZBZitqk3BE9MqAjxBGXJOwrwCHoGVgAZi9q9AIPf4w0u+QyOKShodHRwczovL2Zpbm5leS5jYWxlbmRhci5ldGVybml0eXdhbGwuY29t8BAFZFXFYg7DJJ0OzjmJ0FKWCPEEZck7CvAI0M49IcBR5bf/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPEgoE3IfYTmxxo4W/x/QYp/NGX6Wu93gSQkwbpjpOhZORcI8SDWLLurVQaXHdUuwivCfTfuxYCaq+AzypSGqLDAVocrEgjwIBfgjta16y13Gp4etQOCa9YiKEcM+/9AieG/vZolr3IDCPAgMR2zFCb384CEi8tVuI2fHgLT3I9zpe7oqJTzCcEqxWEI8SAJSdgeeosr7IxdOt8r7f0ipWc8FI6GAhgep8zSRgWikAjxIGxYmtCsC79Tx4z4YsT1WuMo+ycMkwhGQsQltF597cchCPAgCLrBf9vR1Aex6yY+vSkXAvLjMKdMqM/a1g8zNPwLeJcI8SBDCbTk4CczTuiIyZeUyYRVh31BZdjaSd2nU/pBQxu+6QjwIOwqE9/WqGC6CHH0i+tr7edvYX5PstSDf08KmnMqsqCoCPEgQIdEBg358vfek3Qjfyrgl51iCU6WUWmThsGLPDTcB0QI8SAX4dp64iI8pBx+zBqAQwUN6XgZ1cEfT8+2vha/9I1vzwjxWQEAAAAB8YJxvxJJth8OnxIV6UOXveIZAcJPTHcAkWgnucpuYqYAAAAAAP3///8CRhoMAAAAAAAWABTUUZK82uAvbU3vVyaaPIOddZBicwAAAAAAAAAAImog8ARCqgwACAjwIB9TcMDLhzgeS1Uw647lNvCfWECkkUvfrrOe6nay0sGdCAjwICYfs90sbPggoMICyOHGYbmOzop2L9mlnh4xqiBLY7yPCAjxINDiVWOBHnRmGJleQdB9myvJAJbNJ9kciZlTOkgJy89mCAjwIHfxqDLdwycj1Vtyth2CaSDdLQwiey9oV6Xov4stLpWNCAjxIGurJYpKJnKp9+y7MAdC+gXgHOiAu5P3RRUFW9l5hGaCCAjxICXqs9hdY0QMP/MNeqlt6s7xaIYtEXZ1CLvou5gaZNEICAjxIHdYxVeI76NXbT2zHcv6lw+v819Ooib7KWxc1GAsiX2fCAjwICPWdi3uBXOlIdmYi+V9C7wAqLyGE4DMoHD+GtvLizqiCAjwIOF2vENtWN5okEMMS+JSf1SGTY9yYP9j0JjXLbC1s+N1CAjxINWsgCtsPxhRNbe372k8/20WDbiL9e8934hGF256DvRECAjwII3mi+Li06j10ORxg0dYMkcsyGb115Jiqq1YEV3K/u+aCAgABYiWDXPXGQEDw9Qy\",\"created_at\":1707690688,\"id\":\"759f9da5846e936fab06766a524b36ba71c03bbc69ad0944fb8ee4bb1f3dd705\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"07c7896c8cbb97b5d7483097590c9d31b73f35c1ad9e752002bb5c1776cbd852e1d32704333d6930c9bc3e40f8b899a1f2e9f91cc3bf797d86acdecba7792576\",\"tags\":[[\"e\",\"a828a25b4165b14f65a8ae5fd40f9c2fa086275021e15fc0d7234963fb2d504d\"],[\"p\",\"595ca8eaace5899cb6ab7e2542bfc972136376f2eabc09287f1857eb8f167e53\"],[\"alt\",\"NIP-03 time stamp\"]]}" + val otsEvent2 = "{\"content\":\"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\",\"created_at\":1706324334,\"id\":\"2ad074ddb7724eb13b4244b49cf2321b1057f37fdf8ce102e6329b839cf763a9\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"ad7274bb32ba9e9cfdbd52f4887e8a2fda1047c75a7185b2ab7ff254ebac14ed48a2b60737494d655e24c9400eeeec7e29293a77bfcaafaecd94b350c9a2c22b\",\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"p\",\"c31e22c3715c1bde5608b7e0d04904f22f5fc453ba1806d21c9f2382e1e58c6c\"],[\"alt\",\"NIP-03 time stamp\"]]}" + val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}" + + val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6" + + @Before + fun setup() { + OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder()) + } + + @Test + fun verifyNostrEvent() { + val ots = Event.fromJson(otsEvent) as OtsEvent + println(ots.info()) + assertEquals(1707688818L, ots.verify()) + } + + @Test + fun verifyNostrEvent2() { + val ots = Event.fromJson(otsEvent2) as OtsEvent + println(ots.info()) + assertEquals(1706322179L, ots.verify()) + } + + @Test + fun verifyNostrPendingEvent() { + val ots = Event.fromJson(otsPendingEvent) as OtsEvent + println(ots.info()) + assertEquals(null, ots.verify()) + } + + @Test + fun createOTSEventAndVerify() { + val signer = NostrSignerInternal(KeyPair()) + var ots: OtsEvent? = null + + val countDownLatch = CountDownLatch(1) + + OtsEvent.create(otsEvent2Digest, signer) { + ots = it + countDownLatch.countDown() + } + + Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + + println(ots!!.toJson()) + println(ots!!.info()) + + // Should not be valid + assertEquals(null, ots!!.verify()) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 33893662f..ad21a7f0f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -29,7 +29,11 @@ import android.util.Log import androidx.security.crypto.EncryptedSharedPreferences import coil.ImageLoader import coil.disk.DiskCache +import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer +import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder import com.vitorpamplona.amethyst.service.playback.VideoCache +import com.vitorpamplona.quartz.events.OtsEvent +import com.vitorpamplona.quartz.ots.OpenTimestamps import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -65,6 +69,8 @@ class Amethyst : Application() { super.onCreate() instance = this + OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder()) + if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy( ThreadPolicy.Builder().detectAll().penaltyLog().build(), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt new file mode 100644 index 000000000..9b074670a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.ots + +import android.util.Log +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.service.HttpClientManager +import com.vitorpamplona.quartz.ots.BitcoinExplorer +import com.vitorpamplona.quartz.ots.BlockHeader +import com.vitorpamplona.quartz.ots.exceptions.UrlException +import okhttp3.Request + +class OkHttpBlockstreamExplorer : BitcoinExplorer { + /** + * Retrieve the block information from the block hash. + * + * @param hash Hash of the block. + * @return the blockheader of the hash + * @throws Exception desc + */ + override fun block(hash: String): BlockHeader { + val client = HttpClientManager.getHttpClient() + val url = "$BLOCKSTREAM_API_URL/block/$hash" + + val request = + Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .header("Accept", "application/json") + .url(url) + .get() + .build() + + client.newCall(request).execute().use { + if (it.isSuccessful) { + val jsonObject = jacksonObjectMapper().readTree(it.body.string()) + + val blockHeader = BlockHeader() + blockHeader.merkleroot = jsonObject["merkle_root"].asText() + blockHeader.setTime(jsonObject["timestamp"].asInt().toString()) + blockHeader.blockHash = hash + Log.d("OkHttpBlockstreamExplorer", "$BLOCKSTREAM_API_URL/block/$hash") + return blockHeader + } else { + throw UrlException("Couldn't open $url: " + it.message + " " + it.code) + } + } + } + + /** + * Retrieve the block hash from the block height. + * + * @param height Height of the block. + * @return the hash of the block at height height + * @throws Exception desc + */ + @Throws(Exception::class) + override fun blockHash(height: Int): String { + val client = HttpClientManager.getHttpClient() + + val url = "$BLOCKSTREAM_API_URL/block-height/$height" + + val request = + Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .url(url) + .get() + .build() + + client.newCall(request).execute().use { + if (it.isSuccessful) { + val blockHash = it.body.string() + + Log.d("OkHttpBlockstreamExplorer", "$url $blockHash") + return blockHash + } else { + throw UrlException(it.message) + } + } + } + + companion object { + private const val BLOCKSTREAM_API_URL = "https://blockstream.info/api" + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt new file mode 100644 index 000000000..f14d5048a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.ots + +import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.service.HttpClientManager +import com.vitorpamplona.quartz.encoders.Hex +import com.vitorpamplona.quartz.ots.ICalendar +import com.vitorpamplona.quartz.ots.StreamDeserializationContext +import com.vitorpamplona.quartz.ots.Timestamp +import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException +import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException +import com.vitorpamplona.quartz.ots.exceptions.UrlException +import com.vitorpamplona.quartz.ots.http.Request +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody + +/** + * Class representing remote calendar server interface. + */ +class OkHttpCalendar(val url: String) : ICalendar { + /** + * Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest. + * + * @param digest The digest hash to send. + * @return the Timestamp received from the calendar. + * @throws ExceededSizeException if response is too big. + * @throws UrlException if url is not reachable. + * @throws DeserializationException if the data is corrupt + */ + @Throws(ExceededSizeException::class, UrlException::class, DeserializationException::class) + override fun submit(digest: ByteArray): Timestamp { + try { + val client = HttpClientManager.getHttpClient() + val url = "$url/digest" + + val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType() + val requestBody = digest.toRequestBody(mediaType) + + val request = + okhttp3.Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .header("Accept", "application/vnd.opentimestamps.v1") + .header("Content-Type", "application/x-www-form-urlencoded") + .url(url) + .post(requestBody) + .build() + + client.newCall(request).execute().use { + if (it.isSuccessful) { + val ctx = StreamDeserializationContext(it.body.bytes()) + return Timestamp.deserialize(ctx, digest) + } else { + throw UrlException("Failed to open $url") + } + } + } catch (e: ExceededSizeException) { + throw e + } catch (e: DeserializationException) { + throw e + } catch (e: Exception) { + throw UrlException(e.message) + } + } + + /** + * Get a timestamp for a given commitment. + * + * @param commitment The digest hash to send. + * @return the Timestamp from the calendar server (with blockchain information if already written). + * @throws ExceededSizeException if response is too big. + * @throws UrlException if url is not reachable. + * @throws CommitmentNotFoundException if commit is not found. + * @throws DeserializationException if the data is corrupt + */ + @Throws( + DeserializationException::class, + ExceededSizeException::class, + CommitmentNotFoundException::class, + UrlException::class, + ) + override fun getTimestamp(commitment: ByteArray): Timestamp { + try { + val client = HttpClientManager.getHttpClient() + val url = url + "/timestamp/" + Hex.encode(commitment) + + val request = + okhttp3.Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .header("Accept", "application/vnd.opentimestamps.v1") + .header("Content-Type", "application/x-www-form-urlencoded") + .url(url) + .get() + .build() + + client.newCall(request).execute().use { + if (it.isSuccessful) { + val ctx = StreamDeserializationContext(it.body.bytes()) + return Timestamp.deserialize(ctx, commitment) + } else { + throw CommitmentNotFoundException("Calendar response a status code != 200: " + it.code) + } + } + } catch (e: DeserializationException) { + throw e + } catch (e: ExceededSizeException) { + throw e + } catch (e: CommitmentNotFoundException) { + throw e + } catch (e: Exception) { + throw UrlException(e.message) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt new file mode 100644 index 000000000..be082413f --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.ots + +import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.service.HttpClientManager +import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit +import com.vitorpamplona.quartz.ots.StreamDeserializationContext +import com.vitorpamplona.quartz.ots.Timestamp +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import java.util.Optional +import java.util.concurrent.BlockingQueue + +/** + * For making async calls to a calendar server + */ +class OkHttpCalendarAsyncSubmit(private val url: String, private val digest: ByteArray) : ICalendarAsyncSubmit { + private var queue: BlockingQueue>? = null + + fun setQueue(queue: BlockingQueue>?) { + this.queue = queue + } + + @Throws(Exception::class) + override fun call(): Optional { + val client = HttpClientManager.getHttpClient() + val url = "$url/digest" + + val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType() + val requestBody = digest.toRequestBody(mediaType) + + val request = + okhttp3.Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .header("Accept", "application/vnd.opentimestamps.v1") + .header("Content-Type", "application/x-www-form-urlencoded") + .url(url) + .post(requestBody) + .build() + + client.newCall(request).execute().use { + if (it.isSuccessful) { + val ctx = StreamDeserializationContext(it.body.bytes()) + val timestamp = Timestamp.deserialize(ctx, digest) + val of = Optional.of(timestamp) + queue!!.add(of) + return of + } else { + queue!!.add(Optional.empty()) + return Optional.empty() + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt new file mode 100644 index 000000000..b8f9408a8 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.ots + +import com.vitorpamplona.quartz.ots.CalendarBuilder +import com.vitorpamplona.quartz.ots.ICalendar +import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit + +class OkHttpCalendarBuilder : CalendarBuilder { + override fun newSyncCalendar(url: String): ICalendar { + return OkHttpCalendar(url) + } + + override fun newAsyncCalendar( + url: String, + digest: ByteArray, + ): ICalendarAsyncSubmit { + return OkHttpCalendarAsyncSubmit(url, digest) + } +} 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 988ec5836..60dfe74f2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/OtsEvent.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.ots.BlockstreamExplorer +import com.vitorpamplona.quartz.ots.CalendarPureJavaBuilder import com.vitorpamplona.quartz.ots.DetachedTimestampFile import com.vitorpamplona.quartz.ots.Hash import com.vitorpamplona.quartz.ots.OpenTimestamps @@ -58,7 +59,7 @@ class OtsEvent( val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) - val result = OpenTimestamps(BlockstreamExplorer()).verify(detachedOts, digest) + val result = otsInstance.verify(detachedOts, digest) if (result == null || result.isEmpty()) { return null } else { @@ -73,17 +74,19 @@ class OtsEvent( fun info(): String { val detachedOts = DetachedTimestampFile.deserialize(otsByteArray()) - return OpenTimestamps(BlockstreamExplorer()).info(detachedOts) + return otsInstance.info(detachedOts) } companion object { const val KIND = 1040 const val ALT = "Opentimestamps Attestation" + var otsInstance = OpenTimestamps(BlockstreamExplorer(), CalendarPureJavaBuilder()) + fun stamp(eventId: HexKey): ByteArray { val hash = Hash(eventId.hexToByteArray(), OpSHA256._TAG) val file = DetachedTimestampFile.from(hash) - val timestamp = OpenTimestamps(BlockstreamExplorer()).stamp(file) + val timestamp = otsInstance.stamp(file) val detachedToSerialize = DetachedTimestampFile(hash.getOp(), timestamp) return detachedToSerialize.serialize() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java index 8de9f03b8..27c870d8a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/Calendar.java @@ -14,7 +14,7 @@ import java.util.Map; /** * Class representing remote calendar server interface. */ -public class Calendar { +public class Calendar implements ICalendar { private String url; @@ -46,6 +46,7 @@ public class Calendar { * @throws UrlException if url is not reachable. * @throws DeserializationException if the data is corrupt */ + @Override public Timestamp submit(byte[] digest) throws ExceededSizeException, UrlException, DeserializationException { try { @@ -85,6 +86,7 @@ public class Calendar { * @throws CommitmentNotFoundException if commit is not found. * @throws DeserializationException if the data is corrupt */ + @Override public Timestamp getTimestamp(byte[] commitment) throws DeserializationException, ExceededSizeException, CommitmentNotFoundException, UrlException { try { Map headers = new HashMap<>(); diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java index 9d772fe84..08b69a261 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarAsyncSubmit.java @@ -10,12 +10,11 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; /** * For making async calls to a calendar server */ -public class CalendarAsyncSubmit implements Callable> { +public class CalendarAsyncSubmit implements ICalendarAsyncSubmit { private String url; private byte[] digest; diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarBuilder.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarBuilder.java new file mode 100644 index 000000000..f630401e4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarBuilder.java @@ -0,0 +1,6 @@ +package com.vitorpamplona.quartz.ots; + +public interface CalendarBuilder { + public ICalendar newSyncCalendar(String url); + public ICalendarAsyncSubmit newAsyncCalendar(String url, byte[] digest); +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarPureJavaBuilder.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarPureJavaBuilder.java new file mode 100644 index 000000000..d75b65b4c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/CalendarPureJavaBuilder.java @@ -0,0 +1,10 @@ +package com.vitorpamplona.quartz.ots; + +public class CalendarPureJavaBuilder implements CalendarBuilder { + public ICalendar newSyncCalendar(String url) { + return new Calendar(url); + } + public ICalendarAsyncSubmit newAsyncCalendar(String url, byte[] digest) { + return new CalendarAsyncSubmit(url, digest); + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendar.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendar.java new file mode 100644 index 000000000..5f04fa40b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendar.java @@ -0,0 +1,13 @@ +package com.vitorpamplona.quartz.ots; + +import com.vitorpamplona.quartz.ots.exceptions.CommitmentNotFoundException; +import com.vitorpamplona.quartz.ots.exceptions.DeserializationException; +import com.vitorpamplona.quartz.ots.exceptions.ExceededSizeException; +import com.vitorpamplona.quartz.ots.exceptions.UrlException; + +public interface ICalendar { + Timestamp submit(byte[] digest) + throws ExceededSizeException, UrlException, DeserializationException; + + Timestamp getTimestamp(byte[] commitment) throws DeserializationException, ExceededSizeException, CommitmentNotFoundException, UrlException; +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendarAsyncSubmit.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendarAsyncSubmit.java new file mode 100644 index 000000000..9dd821150 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/ICalendarAsyncSubmit.java @@ -0,0 +1,9 @@ +package com.vitorpamplona.quartz.ots; + +import java.util.Optional; +import java.util.concurrent.Callable; + +public interface ICalendarAsyncSubmit extends Callable> { + @Override + Optional call() throws Exception; +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java index 648885639..d422144ce 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/OpenTimestamps.java @@ -33,9 +33,12 @@ import java.util.concurrent.Executors; public class OpenTimestamps { BitcoinExplorer explorer; + CalendarBuilder calBuilder; - public OpenTimestamps(BitcoinExplorer explorer) { + + public OpenTimestamps(BitcoinExplorer explorer, CalendarBuilder builder) { this.explorer = explorer; + this.calBuilder = builder; } /** diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java b/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java index 7a65deac6..0df2db21d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java +++ b/quartz/src/main/java/com/vitorpamplona/quartz/ots/http/Request.java @@ -91,29 +91,4 @@ public class Request implements Callable { return response; } - - public static String urlEncodeUTF8(String s) { - try { - return URLEncoder.encode(s, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new UnsupportedOperationException(e); - } - } - - public static String urlEncodeUTF8(Map map) { - StringBuilder sb = new StringBuilder(); - - for (Map.Entry entry : map.entrySet()) { - if (sb.length() > 0) { - sb.append("&"); - } - - sb.append(String.format("%s=%s", - urlEncodeUTF8(entry.getKey().toString()), - urlEncodeUTF8(entry.getValue().toString()) - )); - } - - return sb.toString(); - } } \ No newline at end of file