lib: add base58 decoder

This commit is contained in:
Oscar Mira 2023-04-20 14:11:17 +02:00
parent 41050d28ed
commit 36be1d209a
2 changed files with 238 additions and 0 deletions

View File

@ -0,0 +1,70 @@
package im.molly.monero.util
import java.nio.ByteBuffer
import java.nio.charset.Charset
const val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
object Decoder {
private val decodingTable = IntArray(128) { ALPHABET.indexOf(it.toChar()) }
private val blockSizes = listOf(0, -1, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8)
fun decode(input: ByteArray): ByteBuffer {
val size = input.size
val needed = 8 * (size / 11) + findOutputBlockSize(size % 11)
val out = ByteBuffer.allocate(needed)
var pos = 0
while (pos + 11 <= size) {
decodeBlock(input, pos, 11, out)
pos += 11
}
val remain = size - pos
if (remain > 0) {
decodeBlock(input, pos, remain, out)
}
out.flip()
return out
}
private fun decodeBlock(block: ByteArray, offset: Int, len: Int, out: ByteBuffer) {
val blockSize = findOutputBlockSize(len)
val newOutPos = out.position() + blockSize
var num = 0uL
var base = 1uL
var zeroes = 0
for (i in (offset + len - 1) downTo offset) {
val c = block[i].toInt()
val digit = decodingTable.getOrElse(c) { -1 }.toULong()
require(digit >= 0uL) { "Invalid symbol" }
if (digit == 0uL) {
zeroes++
} else {
while (zeroes > 0) {
base *= 58u
zeroes--
}
val prod = digit * base
val lastNum = num
num += prod
require((prod / base == digit) && (num > lastNum)) { "Overflow" }
base *= 58u // Never overflows, 58^10 < 2^64
}
}
for (j in 1..blockSize) {
out.put(newOutPos - j, num.toByte())
num = num shr 8
}
require(num == 0uL) { "Overflow" }
out.position(newOutPos)
}
private fun findOutputBlockSize(blockSize: Int): Int = blockSizes[blockSize].also {
require(it >= 0) { "Invalid block size" }
}
}
fun String.decodeBase58(): ByteArray =
Decoder.decode(this.toByteArray(Charset.defaultCharset())).array()

View File

@ -0,0 +1,168 @@
package im.molly.monero.util
import com.google.common.truth.Truth.assertThat
import org.junit.Assert
import org.junit.Test
class Base58Test {
// Test cases from monero unit_tests/base58.cpp
private val base58ToHex = mapOf(
// 2-bytes block
"11" to "00",
"1z" to "39",
"5Q" to "FF",
// 3-bytes block
"111" to "0000",
"11z" to "0039",
"15R" to "0100",
"LUv" to "FFFF",
// 5-bytes block
"11111" to "000000",
"1111z" to "000039",
"11LUw" to "010000",
"2UzHL" to "FFFFFF",
// 6-bytes block
"11111z" to "00000039",
"7YXq9G" to "FFFFFFFF",
// 7-bytes block
"111111z" to "0000000039",
"VtB5VXc" to "FFFFFFFFFF",
// 9-bytes block
"11111111z" to "000000000039",
"3CUsUpv9t" to "FFFFFFFFFFFF",
// 10-bytes block
"111111111z" to "00000000000039",
"Ahg1opVcGW" to "FFFFFFFFFFFFFF",
// 11-bytes block
"1111111111z" to "0000000000000039",
"jpXCZedGfVQ" to "FFFFFFFFFFFFFFFF",
"11111111111" to "0000000000000000",
"11111111112" to "0000000000000001",
"11111111119" to "0000000000000008",
"1111111111A" to "0000000000000009",
"11111111121" to "000000000000003A",
"1Ahg1opVcGW" to "00FFFFFFFFFFFFFF",
"22222222222" to "06156013762879F7",
"1z111111111" to "05E022BA374B2A00",
// Multiple blocks
"1111111111111" to "000000000000000000",
"11111111111111" to "00000000000000000000",
"1111111111111111" to "0000000000000000000000",
"11111111111111111" to "000000000000000000000000",
"111111111111111111" to "00000000000000000000000000",
"11111111111111111111" to "0000000000000000000000000000",
"111111111111111111111" to "000000000000000000000000000000",
"1111111111111111111111" to "00000000000000000000000000000000",
"jpXCZedGfVQ5Q" to "FFFFFFFFFFFFFFFFFF",
"jpXCZedGfVQLUv" to "FFFFFFFFFFFFFFFFFFFF",
"jpXCZedGfVQ2UzHL" to "FFFFFFFFFFFFFFFFFFFFFF",
"jpXCZedGfVQ7YXq9G" to "FFFFFFFFFFFFFFFFFFFFFFFF",
"22222222222VtB5VXc" to "06156013762879F7FFFFFFFFFF",
"jpXCZedGfVQVtB5VXc" to "FFFFFFFFFFFFFFFFFFFFFFFFFF",
"jpXCZedGfVQ3CUsUpv9t" to "FFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"jpXCZedGfVQAhg1opVcGW" to "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"jpXCZedGfVQjpXCZedGfVQ" to "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
)
private val overflows = listOf(
"5R",
"zz",
"LUw",
"zzz",
"2UzHM",
"zzzzz",
"7YXq9H",
"zzzzzz",
"VtB5VXd",
"zzzzzzz",
"3CUsUpv9u",
"zzzzzzzzz",
"Ahg1opVcGX",
"zzzzzzzzzz",
"jpXCZedGfVR",
"zzzzzzzzzzz",
"123456789AB5R",
"123456789ABzz",
"123456789ABLUw",
"123456789ABzzz",
"123456789AB2UzHM",
"123456789ABzzzzz",
"123456789AB7YXq9H",
"123456789ABzzzzzz",
"123456789ABVtB5VXd",
"123456789ABzzzzzzz",
"123456789AB3CUsUpv9u",
"123456789ABzzzzzzzzz",
"123456789ABAhg1opVcGX",
"123456789ABzzzzzzzzzz",
"123456789ABjpXCZedGfVR",
"123456789ABzzzzzzzzzzz",
"zzzzzzzzzzz11",
)
private val invalidSymbols = listOf(
"10",
"11I",
"11O11",
"11l111",
"11_11111111",
"1101111111111",
"11I11111111111111",
"11O1111111111111111111",
"1111111111110",
"111111111111l1111",
"111111111111_111111111",
)
private val invalidLengths = listOf(
"1",
"z",
"1111",
"zzzz",
"11111111",
"zzzzzzzz",
"123456789AB1",
"123456789ABz",
"123456789AB1111",
"123456789ABzzzz",
"123456789AB11111111",
"123456789ABzzzzzzzz",
)
@Test
fun `decode valid base58 strings`() {
base58ToHex.forEach { (input, expected) ->
assertThat(input.decodeBase58()).isEqualTo(expected.parseHex())
}
}
@Test
fun `empty string decodes to zero length`() {
assertThat("".decodeBase58()).hasLength(0)
}
@Test
fun `error on overflows`() {
overflows.forEach {
val thrown =
Assert.assertThrows(IllegalArgumentException::class.java) { it.decodeBase58() }
assertThat(thrown.message).ignoringCase().contains("overflow")
}
}
@Test
fun `error decoding invalid lengths`() {
invalidLengths.forEach {
Assert.assertThrows(IllegalArgumentException::class.java) { it.decodeBase58() }
}
}
@Test
fun `error decoding invalid symbols`() {
invalidSymbols.forEach {
Assert.assertThrows(IllegalArgumentException::class.java) { it.decodeBase58() }
}
}
}