From 2f741779faa5387a5ed3f71447d1464429b976e6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 18 Aug 2023 10:55:06 -0400 Subject: [PATCH] Improves BechUtils performance. --- .../amethyst/benchmark/BechBenchmark.kt | 39 ++++++++ .../quartz/encoders/Bech32Util.kt | 89 ++++++++++++------- 2 files changed, 96 insertions(+), 32 deletions(-) create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/BechBenchmark.kt diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/BechBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/BechBenchmark.kt new file mode 100644 index 000000000..c332fa76c --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/BechBenchmark.kt @@ -0,0 +1,39 @@ +package com.vitorpamplona.amethyst.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.encoders.Bech32 +import com.vitorpamplona.quartz.encoders.Hex +import com.vitorpamplona.quartz.encoders.bechToBytes +import com.vitorpamplona.quartz.encoders.toNpub +import com.vitorpamplona.quartz.events.Event +import junit.framework.TestCase.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BechBenchmark { + + @get:Rule + val benchmarkRule = BenchmarkRule() + + @Test + fun npubEncoding() { + val myUser = Hex.decode("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") + benchmarkRule.measureRepeated { + assertEquals("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",myUser.toNpub()) + } + } + + @Test + fun npubDecoding() { + val myUser = "npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z" + val expected = listOf(70, 12, 37, -26, -126, -3, -89, -125, 43, 82, -47, -14, 45, 61, 34, -77, 23, 109, -105, 47, 96, -36, -36, 50, 18, -19, -116, -110, -17, -123, 6, 92).map { it.toByte() } + benchmarkRule.measureRepeated { + assertEquals(expected, myUser.bechToBytes().toList()) + } + } + +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt index 0a27bfa24..6398225dc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/Bech32Util.kt @@ -30,6 +30,7 @@ private typealias Int5 = Byte */ object Bech32 { const val alphabet: String = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + const val alphabetUpperCase: String = "QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L" enum class Encoding(public val constant: Int) { Bech32(1), @@ -44,20 +45,26 @@ object Bech32 { for (i in 0..alphabet.lastIndex) { map[alphabet[i].code] = i.toByte() } - } - - private fun expand(hrp: String): Array { - val result = Array(hrp.length + 1 + hrp.length) { 0 } - for (i in hrp.indices) { - result[i] = hrp[i].code.shr(5).toByte() - result[hrp.length + 1 + i] = (hrp[i].code and 31).toByte() + for (i in 0..alphabetUpperCase.lastIndex) { + map[alphabetUpperCase[i].code] = i.toByte() } - result[hrp.length] = 0 - return result } - private fun polymod(values: Array, values1: Array): Int { - val GEN = arrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3) + fun expand(hrp: String): Array { + val half = hrp.length + 1 + val size = half + hrp.length + return Array(size) { + when (it) { + in hrp.indices -> hrp[it].code.shr(5).toByte() + in half until size -> (hrp[it - half].code and 31).toByte() + else -> 0 + } + } + } + + private val GEN = arrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3) + + fun polymod(values: Array, values1: Array): Int { var chk = 1 values.forEach { v -> val b = chk shr 25 @@ -83,14 +90,18 @@ object Bech32 { * @return hrp + data encoded as a Bech32 string */ @JvmStatic - public fun encode(hrp: String, int5s: Array, encoding: Encoding): String { + public fun encode(hrp: String, int5s: ArrayList, encoding: Encoding): String { require(hrp.lowercase() == hrp || hrp.uppercase() == hrp) { "mixed case strings are not valid bech32 prefixes" } - val data = int5s.toByteArray().toTypedArray() - val checksum = when (encoding) { - Encoding.Beck32WithoutChecksum -> arrayOf() - else -> checksum(hrp, data, encoding) + val dataWithChecksum = when (encoding) { + Encoding.Beck32WithoutChecksum -> int5s + else -> addChecksum(hrp, int5s, encoding) } - return hrp + "1" + (data + checksum).map { i -> alphabet[i.toInt()] }.toCharArray().concatToString() + + val charArray = CharArray(dataWithChecksum.size) { + alphabet[dataWithChecksum[it].toInt()] + }.concatToString() + + return hrp + "1" + charArray } /** @@ -110,14 +121,21 @@ object Bech32 { */ @JvmStatic public fun decode(bech32: String, noChecksum: Boolean = false): Triple, Encoding> { - require(bech32.lowercase() == bech32 || bech32.uppercase() == bech32) { "mixed case strings are not valid bech32" } - bech32.forEach { require(it.code in 33..126) { "invalid character " } } - val input = bech32.lowercase() - val pos = input.lastIndexOf('1') - val hrp = input.take(pos) + var pos = 0 + bech32.forEachIndexed { index, char -> + require( char.code in 33..126 ) { "invalid character ${char}" } + if (char == '1') { + pos = index + } + } + + val hrp = bech32.take(pos) require(hrp.length in 1..83) { "hrp must contain 1 to 83 characters" } - val data = Array(input.length - pos - 1) { 0 } - for (i in 0..data.lastIndex) data[i] = map[input[pos + 1 + i].code] + + val data = Array(bech32.length - pos - 1) { + map[bech32[pos + 1 + it].code] + } + return if (noChecksum) { Triple(hrp, data, Encoding.Beck32WithoutChecksum) } else { @@ -126,7 +144,7 @@ object Bech32 { Encoding.Bech32m.constant -> Encoding.Bech32m else -> throw IllegalArgumentException("invalid checksum for $bech32") } - Triple(hrp, data.dropLast(6).toTypedArray(), encoding) + Triple(hrp, data.copyOfRange(0, data.size-6), encoding) } } @@ -142,16 +160,23 @@ object Bech32 { return Triple(hrp, five2eight(int5s, 0), encoding) } + val ZEROS = arrayOf(0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte()) + /** * @param hrp Human Readable Part * @param data data (a sequence of 5 bits integers) * @param encoding encoding to use (bech32 or bech32m) * @return a checksum computed over hrp and data */ - private fun checksum(hrp: String, data: Array, encoding: Encoding): Array { + private fun addChecksum(hrp: String, data: ArrayList, encoding: Encoding): ArrayList { val values = expand(hrp) + data - val poly = polymod(values, arrayOf(0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte())) xor encoding.constant - return Array(6) { i -> (poly.shr(5 * (5 - i)) and 31).toByte() } + val poly = polymod(values, ZEROS) xor encoding.constant + + for (i in 0 until 6) { + data.add((poly.shr(5 * (5 - i)) and 31).toByte()) + } + + return data } /** @@ -159,9 +184,9 @@ object Bech32 { * @return a sequence of 5 bits integers */ @JvmStatic - public fun eight2five(input: ByteArray): Array { + public fun eight2five(input: ByteArray): ArrayList { var buffer = 0L - val output = ArrayList() + val output = ArrayList(input.size * 2) //larger array on purpose. Checksum is added later. var count = 0 input.forEach { b -> buffer = (buffer shl 8) or (b.toLong() and 0xff) @@ -172,7 +197,7 @@ object Bech32 { } } if (count > 0) output.add(((buffer shl (5 - count)) and 31).toByte()) - return output.toTypedArray() + return output } /** @@ -182,7 +207,7 @@ object Bech32 { @JvmStatic public fun five2eight(input: Array, offset: Int): ByteArray { var buffer = 0L - val output = ArrayList() + val output = ArrayList(input.size) var count = 0 for (i in offset..input.lastIndex) { val b = input[i]