Skip to content

Instantly share code, notes, and snippets.

@scalahub
Forked from gafiatulin/Bech32.scala
Created May 29, 2022 15:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scalahub/3ff855c3404f7aef953d0c079cf71901 to your computer and use it in GitHub Desktop.
Save scalahub/3ff855c3404f7aef953d0c079cf71901 to your computer and use it in GitHub Desktop.
Scala implementation of Bech32 — general checksummed base32 format.
import scala.util.Try
// BIP173
// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
case object Bech32 {
type Int5 = Byte
final val CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
final val CHARSET_MAP: Map[Char, Int5] = CHARSET.zipWithIndex.toMap.mapValues(_.toByte)
final val CHARSET_REVERSE_MAP: Map[Int5, Char] = CHARSET_MAP.map(_.swap)
final val SEP = '1'
private final val GENERATOR = Seq(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
final def polymod(values: Seq[Int5]): Int = {
var chk = 1
values.foreach { v =>
val b = chk >>> 25
chk = ((chk & 0x1ffffff) << 5) ^ v
(0 until 5).foreach{
case i if ((b >>> i) & 1) != 0 =>
chk = chk ^ GENERATOR(i)
case _ => ()
}
}
chk
}
final def hrpExpand(s: String): Seq[Int5] = {
val b = Array.newBuilder[Int5]
s.foreach{ c =>
b += (c.toInt >>> 5).toByte
}
b += 0.toByte
s.foreach{ c =>
b += (c.toInt & 31).toByte
}
b.result()
}
final def verifyCheckSum(hrp: String, data: Seq[Int5]): Boolean =
polymod(hrpExpand(hrp) ++ data) == 1
final def createChecksum(hrp: String, data: Seq[Int5]): Seq[Int5] = {
val values = hrpExpand(hrp) ++ data
val poly = polymod(values ++ Seq(0.toByte, 0.toByte, 0.toByte, 0.toByte, 0.toByte, 0.toByte)) ^ 1.toByte
(0 to 5).map(i => ((poly >>> 5 * (5 - i)) & 31).toByte)
}
final def decode(bech32: String): Try[(String, Seq[Int5])] = Try{
val l = bech32.length
require(l >= 8 && l <= 90, s"Invalid Bech32: $bech32 (length $l). Valid length range: 8-90 characters.")
require(bech32.forall(c => c.isLower || c.isDigit ) || bech32.forall(c => c.isUpper || c.isDigit), s"Invalid Bech32: $bech32. Mixed case.")
val sepPosition = bech32.lastIndexOf(SEP)
require(sepPosition != -1, s"Invalid Bech32: $bech32. Missing separator $SEP.")
val input = bech32.toLowerCase()
val hrp = input.take(sepPosition)
val data = input.drop(sepPosition + 1).map(CHARSET_MAP)
require(hrp.length >= 1, s"Invalid Bech32: $bech32. Invalid hrp length ${hrp.length}.")
require(data.length >= 6, s"Invalid Bech32: $bech32. Invalid data length ${data.length}.")
require(verifyCheckSum(hrp, data), s"Invalid checksum for $bech32")
(hrp, data.dropRight(6))
}.recover{
case _: java.util.NoSuchElementException =>
throw new IllegalArgumentException(s"requirement failed: Invalid Bech32: $bech32. Invalid Character. Valid (both cases): ${CHARSET.mkString("[", ",", "]")}")
case t =>
throw new IllegalArgumentException(s"requirement failed: Invalid Bech32: $bech32. " + t.getMessage)
}
final def encode(hrp: String, data: Seq[Int5]): Try[String] = Try{
require(hrp.length >= 1, s"Invalid hrp length ${hrp.length}.")
hrp + SEP + (data ++ createChecksum(hrp, data)).map(CHARSET_REVERSE_MAP).mkString
}.recover{
case _: java.util.NoSuchElementException =>
throw new IllegalArgumentException(s"requirement failed: Invalid data: $data. Valid data should contain only UInt5 values.")
case t =>
throw new IllegalArgumentException(s"requirement failed: Invalid hrp: $hrp or data: $data. " + t.getMessage)
}
def to5Bit(input: Seq[Byte]): Seq[Int5] = {
var buffer = 0L
var count = 0
val builder = Array.newBuilder[Byte]
input.foreach(b => {
buffer = (buffer << 8) | (b & 0xff)
count = count + 8
while (count >= 5) {
builder += ((buffer >> (count - 5)) & 31).toByte
count = count - 5
}
})
if (count > 0) builder += ((buffer << (5 - count)) & 31).toByte
builder.result()
}
def from5Bit(input: Seq[Int5]): Seq[Byte] = {
var buffer = 0L
var count = 0
val builder = Array.newBuilder[Byte]
input.foreach(b => {
buffer = (buffer << 5) | (b & 31)
count = count + 5
while (count >= 8) {
builder += ((buffer >> (count - 8)) & 0xff).toByte
count = count - 8
}
})
require(count <= 4)
require((buffer & ((1 << count) - 1)) == 0)
builder.result()
}
}
import org.scalatest.{Inspectors, Matchers, WordSpec}
import scala.util.Success
class Bech32Test extends WordSpec with Matchers with Inspectors{
//https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#test-vectors
val testData = Seq(
"A12UEL5L",
"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs",
"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw",
"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j",
"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w"
)
"Bech32" must {
"decode" in {
forAll(testData){ s =>
Bech32.decode(s) shouldBe a[Success[_]]
}
}
"round trip" in {
forAll(testData){ s =>
Bech32.decode(s).flatMap((Bech32.encode _).tupled) shouldBe Success(s.toLowerCase)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment