Moves Event ID hashing to a new class

This commit is contained in:
Vitor Pamplona
2025-01-06 15:13:14 -05:00
parent 5f577df819
commit 1430ba4745
13 changed files with 437 additions and 233 deletions
@@ -0,0 +1,109 @@
/**
* 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.quartz.benchmark
val reqResponseEvent =
"[\"EVENT\",\"40b9\",{\"id\":\"48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf\"," +
"\"kind\":1,\"pubkey\":\"3d842afecd5e293f28b6627933704a3fb8ce153aa91d790ab11f6a752d44a42d\"," +
"\"created_at\":1677940007,\"content\":" +
"\"I got asked about follower count again today. Why does my follower count go down when " +
"I delete public relays (in our list) and replace them with filter.nostr.wine? \\n\\nIll " +
"give you one final explanation to rule them all. First, lets go over how clients calculate " +
"your follower count.\\n\\n1. Your client sends a request to all your connected relays asking " +
"for accounts who follow you\\n2. Relays answer back with the events requested\\n3. The client " +
"aggregates the event total and displays it\\n\\nEach relay has a set limit on how many stored " +
"events it will return per request. For some relays its 500, others 1000, some as high as 5000. " +
"Lets say for simplicity that all your public relays use 500 as their limit. If you ask 10 " +
"relays for your followers the max possible answer you can get is 5000. That wont change if " +
"you have 20,000 followers or 100,000. You may get back a “different” 5000 each time, but youll " +
"still cap out at 5000 because that is the most events your client will receive.\u2028\u2028Our " +
"limit on filter.nostr.wine is 2000 events. If you replace 10 public relays with only " +
"filter.nostr.wine, the MOST followers you will ever get back from our filter relay is 2000. " +
"That doesnt mean you only have 2000 followers or that your reach is reduced in any way.\\n\\nAs " +
"long as you are writing to and reading from the same public relays, neither your reach nor any " +
"content was lost. That concludes my TED talk. I hope you all have a fantastic day and weekend.\"," +
"\"tags\":[],\"sig\":\"dcaf8ab98bb9179017b35bd814092850d1062b26c263dff89fb1ae8c019a324139d1729012d" +
"9d05ff0a517f76b1117d869b2cc7d36bea8aa5f4b94c5e2548aa8\"}]"
val largeKind1Event =
"""
{
"content": "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ",
"created_at": 1683596206,
"id": "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758",
"kind": 1,
"pubkey": "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d",
"sig": "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce",
"tags": [
[
"e",
"27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3",
"",
"root"
],
[
"e",
"be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc",
"",
"reply"
],
[
"p",
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"
],
[
"p",
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"
],
[
"p",
"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"
],
[
"p",
"ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"
],
[
"p",
"0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"
],
[
"p",
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"
],
[
"p",
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"
],
[
"p",
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"
],
[
"p",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
]
],
"seenOn": [
"wss://nostr.wine/"
]
}
"""
@@ -23,18 +23,13 @@ package com.vitorpamplona.quartz.benchmark
import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.crypto.sha256Hash
import com.vitorpamplona.quartz.encoders.Nip01Serializer
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventFactory import com.vitorpamplona.quartz.events.EventFactory
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.security.MessageDigest
/** /**
* Benchmark, which will execute on an Android device. * Benchmark, which will execute on an Android device.
@@ -44,199 +39,27 @@ import java.security.MessageDigest
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class EventBenchmark { class EventBenchmark {
val payload1 =
"[\"EVENT\",\"40b9\",{\"id\":\"48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf\"," +
"\"kind\":1,\"pubkey\":\"3d842afecd5e293f28b6627933704a3fb8ce153aa91d790ab11f6a752d44a42d\"," +
"\"created_at\":1677940007,\"content\":" +
"\"I got asked about follower count again today. Why does my follower count go down when " +
"I delete public relays (in our list) and replace them with filter.nostr.wine? \\n\\nIll " +
"give you one final explanation to rule them all. First, lets go over how clients calculate " +
"your follower count.\\n\\n1. Your client sends a request to all your connected relays asking " +
"for accounts who follow you\\n2. Relays answer back with the events requested\\n3. The client " +
"aggregates the event total and displays it\\n\\nEach relay has a set limit on how many stored " +
"events it will return per request. For some relays its 500, others 1000, some as high as 5000. " +
"Lets say for simplicity that all your public relays use 500 as their limit. If you ask 10 " +
"relays for your followers the max possible answer you can get is 5000. That wont change if " +
"you have 20,000 followers or 100,000. You may get back a “different” 5000 each time, but youll " +
"still cap out at 5000 because that is the most events your client will receive.\u2028\u2028Our " +
"limit on filter.nostr.wine is 2000 events. If you replace 10 public relays with only " +
"filter.nostr.wine, the MOST followers you will ever get back from our filter relay is 2000. " +
"That doesnt mean you only have 2000 followers or that your reach is reduced in any way.\\n\\nAs " +
"long as you are writing to and reading from the same public relays, neither your reach nor any " +
"content was lost. That concludes my TED talk. I hope you all have a fantastic day and weekend.\"," +
"\"tags\":[],\"sig\":\"dcaf8ab98bb9179017b35bd814092850d1062b26c263dff89fb1ae8c019a324139d1729012d" +
"9d05ff0a517f76b1117d869b2cc7d36bea8aa5f4b94c5e2548aa8\"}]"
val payload2 =
"""
{
"content": "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ",
"created_at": 1683596206,
"id": "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758",
"kind": 1,
"pubkey": "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d",
"sig": "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce",
"tags": [
[
"e",
"27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3",
"",
"root"
],
[
"e",
"be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc",
"",
"reply"
],
[
"p",
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"
],
[
"p",
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"
],
[
"p",
"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"
],
[
"p",
"ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"
],
[
"p",
"0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"
],
[
"p",
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"
],
[
"p",
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"
],
[
"p",
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"
],
[
"p",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
]
],
"seenOn": [
"wss://nostr.wine/"
]
}
"""
@get:Rule val benchmarkRule = BenchmarkRule() @get:Rule val benchmarkRule = BenchmarkRule()
@Test @Test
fun parseREQString() { fun parseREQString() {
benchmarkRule.measureRepeated { Event.mapper.readTree(payload1) } benchmarkRule.measureRepeated { Event.mapper.readTree(reqResponseEvent) }
} }
@Test @Test
fun parseEvent() { fun parseEvent() {
val msg = Event.mapper.readTree(payload1) val msg = Event.mapper.readTree(reqResponseEvent)
benchmarkRule.measureRepeated { Event.fromJson(msg[2]) } benchmarkRule.measureRepeated { Event.fromJson(msg[2]) }
} }
@Test @Test
fun checkSignature() { fun checkSignature() {
val msg = Event.mapper.readTree(payload1) val msg = Event.mapper.readTree(reqResponseEvent)
val event = Event.fromJson(msg[2]) val event = Event.fromJson(msg[2])
benchmarkRule.measureRepeated { benchmarkRule.measureRepeated {
// Should pass // Should pass
assertTrue(event.hasVerifiedSignature()) assertTrue(event.hasValidSignature())
}
}
@Test
fun checkIDHashPayload1() {
val msg = Event.mapper.readTree(payload1)
val event = Event.fromJson(msg[2])
benchmarkRule.measureRepeated {
// Should pass
assertTrue(event.hasCorrectIDHash())
}
}
@Test
fun toMakeJsonForID() {
val event = Event.fromJson(payload2)
benchmarkRule.measureRepeated { assertNotNull(event.makeJsonForId()) }
}
@Test
fun sha256() {
val event = Event.fromJson(payload2)
val byteArray = event.makeJsonForId().toByteArray()
benchmarkRule.measureRepeated {
// Should pass
assertNotNull(sha256Hash(byteArray))
}
}
@Test
fun checkIDHashPayload2() {
val event = Event.fromJson(payload2)
benchmarkRule.measureRepeated {
// Should pass
assertTrue(event.hasCorrectIDHash())
}
}
@Test
fun eventSerializerTest() {
val event = Event.fromJson(payload2)
benchmarkRule.measureRepeated {
val mapper = Nip01Serializer.StringWriter()
Nip01Serializer().serializeEventInto(event, mapper)
}
}
val specialEncoders =
"Test\b\bTest\n\nTest\t\tTest\u000c\u000cTest\r\rTest\\Test\\\\Test\"Test/Test//Test"
@Test
fun jsonStringEncoderJackson() {
val jsonMapper = jacksonObjectMapper()
benchmarkRule.measureRepeated {
jsonMapper.writeValueAsString(specialEncoders)
}
}
@Test
fun jsonStringEncoderOurs() {
val serializer = Nip01Serializer()
benchmarkRule.measureRepeated {
serializer.escapeStringInto(specialEncoders, Nip01Serializer.StringWriter())
}
}
@Test
fun jsonStringEncoderSha256Jackson() {
val jsonMapper = jacksonObjectMapper()
benchmarkRule.measureRepeated {
val digest = MessageDigest.getInstance("SHA-256")
digest.update(jsonMapper.writeValueAsString(specialEncoders).toByteArray())
}
}
@Test
fun jsonStringEncoderSha256Ours() {
val serializer = Nip01Serializer()
benchmarkRule.measureRepeated {
serializer.escapeStringInto(specialEncoders, Nip01Serializer.BufferedDigestWriter(MessageDigest.getInstance("SHA-256")))
} }
} }
@@ -0,0 +1,68 @@
/**
* 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.quartz.benchmark
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.events.Event
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Benchmark, which will execute on an Android device.
*
* The body of [BenchmarkRule.measureRepeated] is measured in a loop, and Studio will output the
* result. Modify your code to see how it affects performance.
*/
@RunWith(AndroidJUnit4::class)
class EventHasherBenchmark {
@get:Rule val benchmarkRule = BenchmarkRule()
@Test
fun checkIDHashKind1WihtoutTags() {
val event = Event.fromJson(Event.mapper.readTree(reqResponseEvent))
benchmarkRule.measureRepeated {
// Should pass
assertTrue(event.hasCorrectIDHash())
}
}
@Test
fun checkIDHashKind1WithTags() {
val event = Event.fromJson(largeKind1Event)
benchmarkRule.measureRepeated {
// Should pass
assertTrue(event.hasCorrectIDHash())
}
}
@Test
fun toMakeJsonForID() {
val event = Event.fromJson(largeKind1Event)
benchmarkRule.measureRepeated { assertNotNull(event.generateId()) }
}
}
@@ -0,0 +1,98 @@
/**
* 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.quartz.benchmark
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.encoders.Nip01Serializer
import com.vitorpamplona.quartz.events.Event
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.security.MessageDigest
/**
* Benchmark, which will execute on an Android device.
*
* The body of [BenchmarkRule.measureRepeated] is measured in a loop, and Studio will output the
* result. Modify your code to see how it affects performance.
*/
@RunWith(AndroidJUnit4::class)
class EventSerializerBenchmark {
@get:Rule val benchmarkRule = BenchmarkRule()
@Test
fun eventSerializerJacksonTest() {
val event = Event.fromJson(largeKind1Event)
benchmarkRule.measureRepeated {
event.toJson()
}
}
@Test
fun eventSerializerManualTest() {
val event = Event.fromJson(largeKind1Event)
benchmarkRule.measureRepeated {
val mapper = Nip01Serializer.StringWriter()
Nip01Serializer().serializeEventInto(event, mapper)
}
}
val specialEncoders =
"Test\b\bTest\n\nTest\t\tTest\u000c\u000cTest\r\rTest\\Test\\\\Test\"Test/Test//Test"
@Test
fun jsonStringEncoderJackson() {
val jsonMapper = jacksonObjectMapper()
benchmarkRule.measureRepeated {
jsonMapper.writeValueAsString(specialEncoders)
}
}
@Test
fun jsonStringEncoderOurs() {
val serializer = Nip01Serializer()
benchmarkRule.measureRepeated {
serializer.escapeStringInto(specialEncoders, Nip01Serializer.StringWriter())
}
}
@Test
fun jsonStringEncoderSha256Jackson() {
val jsonMapper = jacksonObjectMapper()
benchmarkRule.measureRepeated {
val digest = MessageDigest.getInstance("SHA-256")
digest.update(jsonMapper.writeValueAsString(specialEncoders).toByteArray())
}
}
@Test
fun jsonStringEncoderSha256Ours() {
val serializer = Nip01Serializer()
benchmarkRule.measureRepeated {
serializer.escapeStringInto(specialEncoders, Nip01Serializer.BufferedDigestWriter(MessageDigest.getInstance("SHA-256")))
}
}
}
@@ -0,0 +1,55 @@
/**
* 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.quartz.benchmark
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.crypto.nip01.EventHasher
import com.vitorpamplona.quartz.crypto.sha256Hash
import com.vitorpamplona.quartz.events.Event
import junit.framework.TestCase.assertNotNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Benchmark, which will execute on an Android device.
*
* The body of [BenchmarkRule.measureRepeated] is measured in a loop, and Studio will output the
* result. Modify your code to see how it affects performance.
*/
@RunWith(AndroidJUnit4::class)
class Sha256Benchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun sha256() {
val event = Event.fromJson(largeKind1Event)
val byteArray = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content).toByteArray()
benchmarkRule.measureRepeated {
// Should pass
assertNotNull(sha256Hash(byteArray))
}
}
}
@@ -0,0 +1,76 @@
/**
* 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.quartz.crypto.nip01
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.vitorpamplona.quartz.crypto.sha256Hash
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.Event.Companion.mapper
class EventHasher {
companion object {
fun makeJsonForId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): String {
val factory = JsonNodeFactory.instance
val rawEvent =
factory.arrayNode(6).apply {
add(0)
add(pubKey)
add(createdAt)
add(kind)
add(
factory.arrayNode(tags.size).apply {
tags.forEach { tag ->
add(
factory.arrayNode(tag.size).apply { tag.forEach { add(it) } },
)
}
},
)
add(content)
}
return mapper.writeValueAsString(rawEvent)
}
fun hashIdBytes(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): ByteArray = sha256Hash(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
fun hashId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): String = hashIdBytes(pubKey, createdAt, kind, tags, content).toHexKey()
}
}
@@ -35,13 +35,13 @@ import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.sha256Hash import com.vitorpamplona.quartz.crypto.nip01.EventHasher
import com.vitorpamplona.quartz.crypto.nip01.EventHasher.Companion.hashId
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.PoWRank import com.vitorpamplona.quartz.encoders.PoWRank
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.nip46.BunkerMessage import com.vitorpamplona.quartz.events.nip46.BunkerMessage
import com.vitorpamplona.quartz.events.nip46.BunkerRequest import com.vitorpamplona.quartz.events.nip46.BunkerRequest
import com.vitorpamplona.quartz.events.nip46.BunkerResponse import com.vitorpamplona.quartz.events.nip46.BunkerResponse
@@ -50,7 +50,6 @@ import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes import com.vitorpamplona.quartz.utils.pointerSizeInBytes
import com.vitorpamplona.quartz.utils.remove import com.vitorpamplona.quartz.utils.remove
import com.vitorpamplona.quartz.utils.startsWith
import java.math.BigDecimal import java.math.BigDecimal
@Immutable @Immutable
@@ -341,9 +340,7 @@ open class Event(
false false
} }
fun makeJsonForId(): String = makeJsonForId(pubKey, createdAt, kind, tags, content) fun generateId(): String = EventHasher.hashId(pubKey, createdAt, kind, tags, content)
fun generateId(): String = sha256Hash(makeJsonForId().toByteArray()).toHexKey()
private class EventDeserializer : StdDeserializer<Event>(Event::class.java) { private class EventDeserializer : StdDeserializer<Event>(Event::class.java) {
override fun deserialize( override fun deserialize(
@@ -476,42 +473,21 @@ open class Event(
fun toJson(event: Event): String = mapper.writeValueAsString(event) fun toJson(event: Event): String = mapper.writeValueAsString(event)
fun makeJsonForId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): String {
val factory = mapper.nodeFactory
val rawEvent =
factory.arrayNode(6).apply {
add(0)
add(pubKey)
add(createdAt)
add(kind)
add(
factory.arrayNode(tags.size).apply {
tags.forEach { tag ->
add(
factory.arrayNode(tag.size).apply { tag.forEach { add(it) } },
)
}
},
)
add(content)
}
return mapper.writeValueAsString(rawEvent)
}
fun generateId( fun generateId(
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
kind: Int, kind: Int,
tags: Array<Array<String>>, tags: Array<Array<String>>,
content: String, content: String,
): ByteArray = CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray()) ): HexKey = EventHasher.hashId(pubKey, createdAt, kind, tags, content)
fun generateIdBytes(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): ByteArray = EventHasher.hashIdBytes(pubKey, createdAt, kind, tags, content)
fun create( fun create(
signer: NostrSigner, signer: NostrSigner,
@@ -21,7 +21,6 @@
package com.vitorpamplona.quartz.events package com.vitorpamplona.quartz.events
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.nip46.NostrConnectEvent import com.vitorpamplona.quartz.events.nip46.NostrConnectEvent
class EventFactory { class EventFactory {
@@ -62,7 +61,7 @@ class EventFactory {
ChatMessageEncryptedFileHeaderEvent.KIND -> { ChatMessageEncryptedFileHeaderEvent.KIND -> {
if (id.isBlank()) { if (id.isBlank()) {
ChatMessageEncryptedFileHeaderEvent( ChatMessageEncryptedFileHeaderEvent(
Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey(), Event.generateId(pubKey, createdAt, kind, tags, content),
pubKey, pubKey,
createdAt, createdAt,
tags, tags,
@@ -76,7 +75,7 @@ class EventFactory {
ChatMessageEvent.KIND -> { ChatMessageEvent.KIND -> {
if (id.isBlank()) { if (id.isBlank()) {
ChatMessageEvent( ChatMessageEvent(
Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey(), Event.generateId(pubKey, createdAt, kind, tags, content),
pubKey, pubKey,
createdAt, createdAt,
tags, tags,
@@ -24,7 +24,6 @@ import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.bytesUsedInMemory
@@ -152,7 +151,7 @@ class Gossip(
val newContent = content ?: "" val newContent = content ?: ""
val newID = val newID =
id?.ifBlank { null } id?.ifBlank { null }
?: Event.generateId(newPubKey, newCreatedAt, newKind, newTags, newContent).toHexKey() ?: Event.generateId(newPubKey, newCreatedAt, newKind, newTags, newContent)
val sig = "" val sig = ""
return EventFactory.create(newID, newPubKey, newCreatedAt, newKind, newTags, newContent, sig) return EventFactory.create(newID, newPubKey, newCreatedAt, newKind, newTags, newContent, sig)
@@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.signers
import com.vitorpamplona.quartz.crypto.nip04.Nip04 import com.vitorpamplona.quartz.crypto.nip04.Nip04
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventFactory import com.vitorpamplona.quartz.events.EventFactory
import com.vitorpamplona.quartz.events.LnZapPrivateEvent import com.vitorpamplona.quartz.events.LnZapPrivateEvent
@@ -87,7 +86,7 @@ abstract class NostrSigner(
content: String, content: String,
onReady: (T) -> Unit, onReady: (T) -> Unit,
) { ) {
val id = Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey() val id = Event.generateId(pubKey, createdAt, kind, tags, content)
onReady( onReady(
EventFactory.create( EventFactory.create(
@@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.signers
import android.util.Log import android.util.Log
import com.goterl.lazysodium.BuildConfig import com.goterl.lazysodium.BuildConfig
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventFactory import com.vitorpamplona.quartz.events.EventFactory
import com.vitorpamplona.quartz.events.LnZapPrivateEvent import com.vitorpamplona.quartz.events.LnZapPrivateEvent
@@ -40,7 +39,7 @@ class NostrSignerExternal(
content: String, content: String,
onReady: (T) -> Unit, onReady: (T) -> Unit,
) { ) {
val id = Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey() val id = Event.generateId(pubKey, createdAt, kind, tags, content)
val event = val event =
Event( Event(
@@ -66,7 +66,7 @@ class NostrSignerSync(
): T? { ): T? {
if (keyPair.privKey == null) return null if (keyPair.privKey == null) return null
val id = Event.generateId(pubKey, createdAt, kind, tags, content) val id = Event.generateIdBytes(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, keyPair.privKey).toHexKey() val sig = CryptoUtils.sign(id, keyPair.privKey).toHexKey()
return EventFactory.create( return EventFactory.create(
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.encoders package com.vitorpamplona.quartz.encoders
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.crypto.nip01.EventHasher
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import org.junit.Test import org.junit.Test
@@ -107,14 +108,14 @@ class Nip01SerializerTest {
@Test() @Test()
fun fastEventSerializerTest() { fun fastEventSerializerTest() {
val event = Event.fromJson(payload2) val event = Event.fromJson(payload2)
val mapper = Nip01Serializer.StringWriter() val mapper = Nip01Serializer.StringWriter()
Nip01Serializer().serializeEventInto(event, mapper) Nip01Serializer().serializeEventInto(event, mapper)
val encoded = mapper.toString() val encoded = mapper.toString()
val eventJson = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content)
assertEquals(event.makeJsonForId(), encoded) assertEquals(eventJson, encoded)
} }
@Test() @Test()
@@ -149,8 +150,9 @@ class Nip01SerializerTest {
Nip01Serializer().serializeEventInto(event, mapper) Nip01Serializer().serializeEventInto(event, mapper)
val encoded = mapper.toString() val encoded = mapper.toString()
val eventJson = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content)
assertEquals(event.makeJsonForId(), encoded) assertEquals(eventJson, encoded)
} }
@Test() @Test()
@@ -171,8 +173,9 @@ class Nip01SerializerTest {
Nip01Serializer().serializeEventInto(event, mapper) Nip01Serializer().serializeEventInto(event, mapper)
val encoded = mapper.toString() val encoded = mapper.toString()
val eventJson = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content)
assertEquals(event.makeJsonForId(), encoded) assertEquals(eventJson, encoded)
} }
@Test() @Test()
@@ -194,15 +197,15 @@ class Nip01SerializerTest {
Nip01Serializer().serializeEventInto(event, mapper) Nip01Serializer().serializeEventInto(event, mapper)
val encoded = mapper.toString() val encoded = mapper.toString()
val eventJson = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content)
assertEquals(event.makeJsonForId(), encoded) assertEquals(eventJson, encoded)
} }
@Test() @Test()
fun fastEventIdCheckTestPayload5() { fun fastEventIdCheckTestPayload5() {
val event = Event.fromJson(payload5) val event = Event.fromJson(payload5)
// assertEquals(event.generateId(), event.generateId2())
assertEquals("d1f097d3d9fcfb00df0c8ab5469be6484b14707d1e947c574ed636281d8dfd26", event.generateId()) assertEquals("d1f097d3d9fcfb00df0c8ab5469be6484b14707d1e947c574ed636281d8dfd26", event.generateId())
} }
} }