Trying to unify NIP01 Serialization with SHA-256 procedures to reduce creation of several bytearray at every verification.

This commit is contained in:
Vitor Pamplona
2024-05-16 13:03:30 -04:00
parent 18d08bf6e0
commit e6d8291f07
4 changed files with 564 additions and 10 deletions
@@ -0,0 +1,276 @@
/**
* 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.encoders
import com.vitorpamplona.quartz.events.Event
import java.nio.ByteBuffer
import java.nio.CharBuffer
import java.nio.charset.CodingErrorAction
import java.security.MessageDigest
class Nip01Serializer {
interface Writer {
fun append(
value: ByteArray,
offset: Int,
length: Int,
)
fun append(
value: String,
offset: Int,
length: Int,
)
fun append(value: ByteArray)
fun append(value: Byte)
fun dump()
}
class BufferedDigestWriter(val digest: MessageDigest) : Writer {
val utf8Encoder =
Charsets.UTF_8.newEncoder()
.onMalformedInput(CodingErrorAction.IGNORE)
.onUnmappableCharacter(CodingErrorAction.IGNORE)
companion object {
const val BUFFER_SIZE = 128
}
private val innerBuffer = ByteArray(BUFFER_SIZE)
private val byteBuffer = ByteBuffer.wrap(innerBuffer)
override fun append(value: Byte) {
if (byteBuffer.position() == byteBuffer.capacity()) {
dump()
}
byteBuffer.put(value)
}
override fun append(value: ByteArray) {
if (value.size >= BUFFER_SIZE) {
dump()
// don't cache if the cache is smaller than the value
digest.update(value)
} else {
if (value.size > byteBuffer.remaining()) {
dump()
}
value.copyInto(innerBuffer, byteBuffer.position(), 0, value.size)
byteBuffer.position(byteBuffer.position() + value.size)
}
}
override fun append(
value: ByteArray,
offset: Int,
length: Int,
) {
if (value.size >= BUFFER_SIZE) {
dump()
// don't cache if the cache is smaller than the value
digest.update(value, offset, length)
} else {
if (length > byteBuffer.remaining()) {
dump()
}
value.copyInto(innerBuffer, byteBuffer.position(), offset, offset + length)
byteBuffer.position(byteBuffer.position() + value.size)
}
}
override fun append(
value: String,
offset: Int,
length: Int,
) {
val toEncode = CharBuffer.wrap(value, offset, offset + length)
while (toEncode.hasRemaining()) {
val result = utf8Encoder.encode(toEncode, byteBuffer, false)
if (result.isOverflow) {
dump()
}
}
}
override fun dump() {
if (byteBuffer.position() > 0) {
digest.update(innerBuffer, 0, byteBuffer.position())
byteBuffer.clear()
}
}
}
class StringWriter : Writer {
private val stringBuilder: StringBuilder = StringBuilder()
override fun append(
value: ByteArray,
offset: Int,
length: Int,
) {
stringBuilder.append(value.decodeToString(offset, offset + length))
}
override fun append(value: ByteArray) {
stringBuilder.append(value.decodeToString())
}
override fun append(
value: String,
offset: Int,
length: Int,
) {
stringBuilder.append(value, offset, offset + length)
}
override fun append(value: Byte) {
stringBuilder.append(value.toInt().toChar())
}
override fun toString(): String {
return stringBuilder.toString()
}
override fun dump() {
}
}
companion object {
private const val DOUBLE_QUOTE_ASCII = 0x22
private const val BACKLASH_ASCII = 0x5C
private const val TAB_ASCII = 0x09
private const val BACKSPACE_ASCII = 0x08
private const val NEWLINE_ASCII = 0x0A
private const val RETURN_ASCII = 0x0D
private const val FORM_FEED_ASCII = 0x0C
private val ESCAPED_DOUBLE_QUOTE = "\\\"".toByteArray()
private val ESCAPED_DOUBLE_BACKLASH = "\\\\".toByteArray()
private val ESCAPED_TAB = "\\t".toByteArray()
private val ESCAPED_BACKSPACE = "\\b".toByteArray()
private val ESCAPED_NEW_LINE = "\\n".toByteArray()
private val ESCAPED_RETURN = "\\r".toByteArray()
private val ESCAPED_FORM_FEED = "\\f".toByteArray()
val ARRAY_ZERO_COMMA_QUOTE = "[0,\"".toByteArray()
val COMMA = ",".toByteArray()
val QUOTE = "\"".toByteArray()
val QUOTE_COMMA = "\",".toByteArray()
val QUOTE_COMMA_QUOTE = "\",\"".toByteArray()
val COMMA_OPEN_ARRAY = ",[".toByteArray()
val COMMA_OPEN_ARRAY_QUOTE = ",[\"".toByteArray()
val OPEN_ARRAY = "[".toByteArray()
val OPEN_ARRAY_QUOTE = "[\"".toByteArray()
val QUOTE_CLOSE_ARRAY = "\"]".toByteArray()
val CLOSE_ARRAY_COMMA_QUOTE = "],\"".toByteArray()
private val MAPPER = Array<ByteArray?>(255) { null }
init {
for (i in 0 until 0x1F) {
MAPPER[i] = String.format("\\u%04x", i.toByte()).toByteArray()
}
MAPPER[DOUBLE_QUOTE_ASCII] = ESCAPED_DOUBLE_QUOTE
MAPPER[BACKLASH_ASCII] = ESCAPED_DOUBLE_BACKLASH
MAPPER[TAB_ASCII] = ESCAPED_TAB
MAPPER[BACKSPACE_ASCII] = ESCAPED_BACKSPACE
MAPPER[NEWLINE_ASCII] = ESCAPED_NEW_LINE
MAPPER[RETURN_ASCII] = ESCAPED_RETURN
MAPPER[FORM_FEED_ASCII] = ESCAPED_FORM_FEED
}
}
fun escapeStringInto(
value: String,
writer: Writer,
) {
var lastNormalSequenceStarts = 0
var lastNormalSequenceLength = 0
for (i in value.indices) {
if (value[i].code >= 255) {
lastNormalSequenceLength++
} else {
val escaped = MAPPER[value[i].code]
if (escaped != null) {
if (lastNormalSequenceLength > 0) {
writer.append(value, lastNormalSequenceStarts, lastNormalSequenceLength)
}
lastNormalSequenceStarts = i + 1
lastNormalSequenceLength = 0
writer.append(escaped)
} else {
lastNormalSequenceLength++
}
}
}
if (lastNormalSequenceLength > 0) {
if (lastNormalSequenceLength == value.length) {
writer.append(value, 0, value.length)
} else {
writer.append(value, lastNormalSequenceStarts, lastNormalSequenceLength)
}
}
}
fun serializeEventInto(
event: Event,
writer: Writer,
) {
writer.append(ARRAY_ZERO_COMMA_QUOTE)
writer.append(event.pubKey.toByteArray())
writer.append(QUOTE_COMMA)
writer.append(event.createdAt.toString().toByteArray())
writer.append(COMMA)
writer.append(event.kind.toString().toByteArray())
writer.append(COMMA_OPEN_ARRAY)
for (index in event.tags.indices) {
val tag = event.tags[index]
if (index > 0) {
writer.append(COMMA_OPEN_ARRAY_QUOTE)
} else {
writer.append(OPEN_ARRAY_QUOTE)
}
for (sIndex in tag.indices) {
if (sIndex > 0) {
writer.append(QUOTE_COMMA_QUOTE)
}
escapeStringInto(tag[sIndex], writer)
}
writer.append(QUOTE_CLOSE_ARRAY)
}
writer.append(CLOSE_ARRAY_COMMA_QUOTE)
escapeStringInto(event.content, writer)
writer.append(QUOTE_CLOSE_ARRAY)
writer.dump()
}
}
@@ -38,11 +38,13 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip01Serializer
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
import java.math.BigDecimal
import java.security.MessageDigest
@Immutable
open class Event(
@@ -273,6 +275,11 @@ open class Event(
return id.equals(generateId())
}
fun hasCorrectIDHash2(): Boolean {
if (id.isEmpty()) return false
return id.equals(generateId2())
}
fun hasVerifiedSignature(): Boolean {
if (id.isEmpty() || sig.isEmpty()) return false
return CryptoUtils.verifySignature(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
@@ -313,6 +320,12 @@ open class Event(
return CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey()
}
fun generateId2(): String {
val sha256 = MessageDigest.getInstance("SHA-256")
Nip01Serializer().serializeEventInto(this, Nip01Serializer.BufferedDigestWriter(sha256))
return sha256.digest().toHexKey()
}
private class EventDeserializer : StdDeserializer<Event>(Event::class.java) {
override fun deserialize(
jp: JsonParser,