Rework Bech32 (#360)

Refactor commonalities between LN and BTC
Add support for RegTest
This commit is contained in:
Torkel Rogstad 2019-02-27 22:55:41 +01:00 committed by Chris Stewart
parent bad37db6fa
commit 25fa009b95
12 changed files with 456 additions and 265 deletions

View file

@ -1,8 +1,28 @@
package org.bitcoins.core.protocol package org.bitcoins.core.protocol
import org.scalatest.{FlatSpec, MustMatchers} import org.bitcoins.core.util.BitcoinSUnitTest
import org.bitcoins.testkit.core.gen.AddressGenerator
/** import scala.util.{Failure, Success}
* Created by chris on 3/23/15.
*/ class AddressTest extends BitcoinSUnitTest {
class AddressTest extends FlatSpec with MustMatchers {}
behavior of "Address"
it must "have serialization symmetry" in {
forAll(AddressGenerator.address) { addr =>
val fromSPK = Address
.fromScriptPubKey(addr.scriptPubKey, addr.networkParameters)
fromSPK match {
case Success(newAddr) => assert(newAddr.value == addr.value)
case Failure(exception) => fail(exception.getMessage)
}
val fromStringT = Address.fromString(addr.value)
fromStringT match {
case Success(newAddr) => assert(newAddr.value == addr.value)
case Failure(exception) => fail(exception.getMessage)
}
}
}
}

View file

@ -1,18 +1,31 @@
package org.bitcoins.core.protocol package org.bitcoins.core.protocol
import org.bitcoins.core.util.Bech32
import org.bitcoins.testkit.core.gen.ln.LnInvoiceGen
import org.bitcoins.testkit.core.gen.{ import org.bitcoins.testkit.core.gen.{
AddressGenerator, AddressGenerator,
ChainParamsGenerator, ChainParamsGenerator,
ScriptGenerators ScriptGenerators
} }
import org.bitcoins.core.util.{Bech32, BitcoinSLogger}
import org.scalacheck.{Prop, Properties} import org.scalacheck.{Prop, Properties}
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.util.{Random, Success} import scala.util.{Random, Success}
class Bech32Spec extends Properties("Bech32Spec") { class Bech32Spec extends Properties("Bech32Spec") {
private val logger = BitcoinSLogger.logger property("split all LN invoices into HRP and data") = {
Prop.forAll(LnInvoiceGen.lnInvoice) { invoice =>
val splitT = Bech32.splitToHrpAndData(invoice.toString)
splitT.isSuccess
}
}
property("split all Bech32 addresses into HRP and data") = {
Prop.forAll(AddressGenerator.bech32Address) { address =>
val splitT = Bech32.splitToHrpAndData(address.value)
splitT.isSuccess
}
}
property("serialization symmetry") = { property("serialization symmetry") = {
Prop.forAll(ScriptGenerators.witnessScriptPubKey, Prop.forAll(ScriptGenerators.witnessScriptPubKey,
@ -25,27 +38,25 @@ class Bech32Spec extends Properties("Bech32Spec") {
} }
property("checksum must not work if we modify a char") = { property("checksum must not work if we modify a char") = {
Prop.forAll(AddressGenerator.bech32Address) { Prop.forAll(AddressGenerator.bech32Address) { addr: Bech32Address =>
case addr: Bech32Address => val old = addr.value
val old = addr.value val rand = Math.abs(Random.nextInt)
val rand = Math.abs(Random.nextInt) val idx = rand % old.length
val idx = rand % old.size val (f, l) = old.splitAt(idx)
val (f, l) = old.splitAt(idx) val replacementChar = pickReplacementChar(l.head)
val replacementChar = pickReplacementChar(l.head) val replaced = f ++ Seq(replacementChar) ++ l.tail
val replaced = f ++ Seq(replacementChar) ++ l.tail //should fail because we replaced a char in the addr, so checksum invalid
//should fail because we replaced a char in the addr, so checksum invalid Bech32Address.fromString(replaced).isFailure
Bech32Address.fromString(replaced).isFailure
} }
} }
property("must fail if we have a mixed case") = { property("must fail if we have a mixed case") = {
Prop.forAllNoShrink(AddressGenerator.bech32Address) { Prop.forAllNoShrink(AddressGenerator.bech32Address) { addr: Bech32Address =>
case addr: Bech32Address => val old = addr.value
val old = addr.value val replaced = switchCaseRandChar(old)
val replaced = switchCaseRandChar(old) //should fail because we we switched the case of a random char
//should fail because we we switched the case of a random char val actual = Bech32Address.fromString(replaced)
val actual = Bech32Address.fromString(replaced) actual.isFailure
actual.isFailure
} }
} }
@ -61,7 +72,7 @@ class Bech32Spec extends Properties("Bech32Spec") {
@tailrec @tailrec
private def switchCaseRandChar(addr: String): String = { private def switchCaseRandChar(addr: String): String = {
val rand = Math.abs(Random.nextInt) val rand = Math.abs(Random.nextInt)
val idx = rand % addr.size val idx = rand % addr.length
val (f, l) = addr.splitAt(idx) val (f, l) = addr.splitAt(idx)
if (l.head.isDigit) { if (l.head.isDigit) {
switchCaseRandChar(addr) switchCaseRandChar(addr)

View file

@ -2,49 +2,28 @@ package org.bitcoins.core.protocol
import org.bitcoins.core.config.{MainNet, TestNet3} import org.bitcoins.core.config.{MainNet, TestNet3}
import org.bitcoins.core.crypto.ECPublicKey import org.bitcoins.core.crypto.ECPublicKey
import org.bitcoins.testkit.core.gen.NumberGenerator
import org.bitcoins.core.number.{UInt5, UInt8} import org.bitcoins.core.number.{UInt5, UInt8}
import org.bitcoins.core.protocol.ln.LnHumanReadablePart
import org.bitcoins.core.protocol.ln.currency.PicoBitcoins
import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.script._
import org.bitcoins.core.util.{Bech32, BitcoinSUnitTest} import org.bitcoins.core.util.{Bech32, BitcoinSUnitTest}
import org.scalacheck.Gen import org.bitcoins.testkit.core.gen.NumberGenerator
import scala.util.{Success, Try} import scala.util.{Failure, Success}
class Bech32Test extends BitcoinSUnitTest { class Bech32Test extends BitcoinSUnitTest {
override implicit val generatorDrivenConfig = generatorDrivenConfigNewCode override implicit val generatorDrivenConfig: PropertyCheckConfiguration =
"Bech32" must "validly encode the test vectors from bitcoin core correctly" in { generatorDrivenConfigNewCode
val valid = Seq(
"A12UEL5L",
"a12uel5l",
"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs",
"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw",
"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j",
"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w",
"?1ezyfcl"
)
val results: Seq[Try[Bech32Address]] =
valid.map(Bech32Address.fromString(_))
results.exists(_.isFailure) must be(false)
}
it must "mark invalid test vectors as invalid from bitcoin core" in { behavior of "Bech32"
val invalid = Seq(
" 1nwldj5", it must "decode a regtest address from Bitcoin Core" in {
"\\x7f\"\"1axkwrx", val addrStr = "bcrt1qq6w6pu6zq90az9krn53zlkvgyzkyeglzukyepf"
"\\x80\"\"1eym55h", val addrT = Address.fromString(addrStr)
"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", addrT match {
"pzry9x0s0muk", case Success(addr: Bech32Address) => assert(addr.value == addrStr)
"1pzry9x0s0muk", case _ => fail()
"x1b4n0q5v", }
"li1dgmt3",
"de1lg7wt\\xff",
"A1G7SGD8",
"10a06t8",
"1qzzfhee"
)
val results: Seq[Try[Bech32Address]] =
invalid.map(Bech32Address.fromString(_))
results.exists(_.isSuccess) must be(false)
} }
it must "follow the example in BIP173" in { it must "follow the example in BIP173" in {
@ -82,21 +61,103 @@ class Bech32Test extends BitcoinSUnitTest {
mp2wshDecoded must be(Success(p2wsh)) mp2wshDecoded must be(Success(p2wsh))
} }
it must "expand the human readable part correctly" in { it must "expand the human readable part correctly - BTC" in {
Bech32Address.hrpExpand(bc) must be( BtcHumanReadablePart.bc.expand must be(
Vector(UInt5(3), UInt5(3), UInt5(0), UInt5(2), UInt5(3))) Vector(UInt5(3), UInt5(3), UInt5(0), UInt5(2), UInt5(3)))
Bech32Address.hrpExpand(tb) must be( BtcHumanReadablePart.tb.expand must be(
Vector(UInt5(3), UInt5(3), UInt5(0), UInt5(20), UInt5(2))) Vector(UInt5(3), UInt5(3), UInt5(0), UInt5(20), UInt5(2)))
BtcHumanReadablePart.bcrt.expand must be(
Vector(UInt5(3),
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(0),
UInt5(2),
UInt5(3),
UInt5(18),
UInt5(20)))
}
it must "expand the human readable part correctly - LN no amount" in {
LnHumanReadablePart.lnbc(None).expand must be(
Vector(
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(0),
UInt5(12),
UInt5(14),
UInt5(2),
UInt5(3)
))
LnHumanReadablePart.lntb(None).expand must be(
Vector(
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(0),
UInt5(12),
UInt5(14),
UInt5(20),
UInt5(2)
))
LnHumanReadablePart.lnbcrt(None).expand must be(
Vector(
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(0),
UInt5(12),
UInt5(14),
UInt5(2),
UInt5(3),
UInt5(18),
UInt5(20)
))
}
it must "expand the human readable part correctly - LN with amount" in {
val hrp = LnHumanReadablePart.lnbc(Some(PicoBitcoins(724)))
val expanded = hrp.expand
assert(
expanded == Vector(
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(3),
UInt5(1),
UInt5(1),
UInt5(1),
UInt5(3),
UInt5(0),
UInt5(12),
UInt5(14),
UInt5(2),
UInt5(3),
UInt5(23),
UInt5(18),
UInt5(20),
UInt5(16)
))
} }
it must "encode 0 byte correctly" in { it must "encode 0 byte correctly" in {
val addr = Bech32Address(bc, Vector(UInt5.zero)) val addr = Bech32Address(BtcHumanReadablePart.bc, Vector(UInt5.zero))
addr.value must be("bc1q9zpgru") addr.value must be("bc1q9zpgru")
} }
it must "create the correct checksum for a 0 byte address" in { it must "create the correct checksum for a 0 byte address" in {
val checksum = Bech32Address.createChecksum(bc, Vector(UInt5.zero)) val checksum =
Bech32Address.createChecksum(BtcHumanReadablePart.bc, Vector(UInt5.zero))
checksum must be(Seq(5, 2, 1, 8, 3, 28).map(i => UInt5(i.toByte))) checksum must be(Seq(5, 2, 1, 8, 3, 28).map(i => UInt5(i.toByte)))
checksum.map(ch => Bech32.charset(ch.toInt)).mkString must be("9zpgru") checksum.map(ch => Bech32.charset(ch.toInt)).mkString must be("9zpgru")
} }
@ -111,7 +172,8 @@ class Bech32Test extends BitcoinSUnitTest {
val encoded1 = Bech32.from8bitTo5bit(Vector(z, UInt8.one)) val encoded1 = Bech32.from8bitTo5bit(Vector(z, UInt8.one))
encoded1 must be(Seq(fz, fz, fz, UInt5(16.toByte))) encoded1 must be(Seq(fz, fz, fz, UInt5(16.toByte)))
//130.toByte == -126 //130.toByte == -126
val encoded2 = Bech32.from8bitTo5bit(Vector(130).map(i => UInt8(i.toShort))) val encoded2 =
Bech32.from8bitTo5bit(Vector(130).map(i => UInt8(i.toShort)))
encoded2 must be(Seq(16, 8).map(i => UInt5(i.toByte))) encoded2 must be(Seq(16, 8).map(i => UInt5(i.toByte)))
//130.toByte == -126 //130.toByte == -126

View file

@ -1,11 +1,11 @@
package org.bitcoins.core.protocol package org.bitcoins.core.protocol
import org.bitcoins.core.config.MainNet import org.bitcoins.core.config.{MainNet, RegTest, TestNet3}
import org.bitcoins.core.crypto.Sha256Hash160Digest import org.bitcoins.core.crypto.Sha256Hash160Digest
import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.core.protocol.script.ScriptPubKey
import org.scalatest.{FlatSpec, MustMatchers} import org.scalatest.{FlatSpec, MustMatchers}
import scala.util.Try import scala.util.{Failure, Success, Try}
class BitcoinAddressTest extends FlatSpec with MustMatchers { class BitcoinAddressTest extends FlatSpec with MustMatchers {
@ -27,10 +27,44 @@ class BitcoinAddressTest extends FlatSpec with MustMatchers {
P2SHAddress.isValid(address) must be(false) P2SHAddress.isValid(address) must be(false)
} }
"bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" must "be a valid Bech32 address" in {
val address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
Address.fromString(address) match {
case Success(bech: Bech32Address) =>
assert(bech.networkParameters == MainNet)
case Success(other: BitcoinAddress) =>
fail(s"Wrong address type! Got $other")
case Failure(exception) => fail(exception.getMessage)
}
}
"tb1q9yqzjcywvuy9lz2vuvv6xmkhe7zg9kkp35mdrn" must "be a valid Bech32 address" in {
val address = "tb1q9yqzjcywvuy9lz2vuvv6xmkhe7zg9kkp35mdrn"
Address.fromString(address) match {
case Success(bech: Bech32Address) =>
assert(bech.networkParameters == TestNet3)
case Success(other: BitcoinAddress) =>
fail(s"Wrong address type! Got $other")
case Failure(exception) => fail(exception.getMessage)
}
}
"bcrt1q03nrxf0s99sny47mp47a8grdvrcph2v4c78rvd" must "be a valid Bec32 addres" in {
val address = "bcrt1q03nrxf0s99sny47mp47a8grdvrcph2v4c78rvd"
Address.fromString(address) match {
case Success(bech: Bech32Address) =>
assert(bech.networkParameters == RegTest)
case Success(other: BitcoinAddress) =>
fail(s"Wrong address type! Got $other")
case Failure(exception) => fail(exception.getMessage)
}
}
"The empty string" must "not be a valid bitcoin address" in { "The empty string" must "not be a valid bitcoin address" in {
BitcoinAddress.fromString("").isFailure must be(true) BitcoinAddress.fromString("").isFailure must be(true)
Try(BitcoinAddress.fromStringExn("")).isFailure must be(true) Try(BitcoinAddress.fromStringExn("")).isFailure must be(true)
} }
"A string that is 25 characters long" must "not be a valid bitcoin address" in { "A string that is 25 characters long" must "not be a valid bitcoin address" in {
val address = "3J98t1WpEZ73CNmQviecrnyiW" val address = "3J98t1WpEZ73CNmQviecrnyiW"
BitcoinAddress.fromString(address).isFailure must be(true) BitcoinAddress.fromString(address).isFailure must be(true)

View file

@ -0,0 +1,22 @@
package org.bitcoins.core.protocol
import org.bitcoins.core.config.{MainNet, RegTest, TestNet3}
import org.scalatest.{FlatSpec, MustMatchers}
import scala.util.Success
class BtcHumanReadablePartTest extends FlatSpec with MustMatchers {
import BtcHumanReadablePart._
"HumanReadablePart" must "match the correct hrp with the correct string" in {
BtcHumanReadablePart("tb") must be(Success(tb))
BtcHumanReadablePart("bc") must be(Success(bc))
BtcHumanReadablePart("bcrt") must be(Success(bcrt))
}
it must "match the correct hrp with the correct network" in {
BtcHumanReadablePart(TestNet3) must be(tb)
BtcHumanReadablePart(MainNet) must be(bc)
BtcHumanReadablePart(RegTest) must be(bcrt)
}
}

View file

@ -410,7 +410,7 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
val invoice = val invoice =
LnInvoice.build(hrp = hrpEmpty, lnTags = tags, privateKey = privKey) LnInvoice.build(hrp = hrpEmpty, lnTags = tags, privateKey = privKey)
assert(invoice.isValidSignature()) assert(invoice.isValidSignature)
} }
it must "handle the weird case if sigdata being exactly on a byte boundary, which means we need to pad the sigdata with a zero byte" in { it must "handle the weird case if sigdata being exactly on a byte boundary, which means we need to pad the sigdata with a zero byte" in {
@ -439,7 +439,7 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
invoice.lnTags.expiryTime.get.u32 must be(UInt32(3600)) invoice.lnTags.expiryTime.get.u32 must be(UInt32(3600))
invoice.isValidSignature() must be(true) invoice.isValidSignature must be(true)
invoice.signatureData.toHex must be( invoice.signatureData.toHex must be(
"6c6e74623130306e0b851aec410d1ae02e6dcc82f0480d4d39e3e1ada8ca5170d6ad19391d7b403a4b1ee61d57e741a72bd91323ab930ba34b7b7111d1898181818161131b430b73732b6111d113a3930b232b991161132bb32b73a111d1139bab139b1b934b1329116113abab4b2111d111818189899191999969a1a1a9a969b1b1b9b969c1c1c9c96b0b0b13131b1b23232b2b33311161132bc31b430b733b2911d113134ba3334b732bc11161139bcb6b137b6111d11212a21aaa9a2113e8c018e100") "6c6e74623130306e0b851aec410d1ae02e6dcc82f0480d4d39e3e1ada8ca5170d6ad19391d7b403a4b1ee61d57e741a72bd91323ab930ba34b7b7111d1898181818161131b430b73732b6111d113a3930b232b991161132bb32b73a111d1139bab139b1b934b1329116113abab4b2111d111818189899191999969a1a1a9a969b1b1b9b969c1c1c9c96b0b0b13131b1b23232b2b33311161132bc31b430b733b2911d113134ba3334b732bc11161139bcb6b137b6111d11212a21aaa9a2113e8c018e100")

View file

@ -1,5 +1,5 @@
package org.bitcoins.core.protocol package org.bitcoins.core.protocol
import org.bitcoins.core.config.{MainNet, RegTest, TestNet3, _} import org.bitcoins.core.config.{MainNet, TestNet3, _}
import org.bitcoins.core.crypto._ import org.bitcoins.core.crypto._
import org.bitcoins.core.number.{UInt5, UInt8} import org.bitcoins.core.number.{UInt5, UInt8}
import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.script._
@ -7,7 +7,6 @@ import org.bitcoins.core.script.constant.ScriptConstant
import org.bitcoins.core.util._ import org.bitcoins.core.util._
import scodec.bits.ByteVector import scodec.bits.ByteVector
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
sealed abstract class Address { sealed abstract class Address {
@ -66,11 +65,11 @@ sealed abstract class P2SHAddress extends BitcoinAddress {
*/ */
sealed abstract class Bech32Address extends BitcoinAddress { sealed abstract class Bech32Address extends BitcoinAddress {
def hrp: HumanReadablePart def hrp: BtcHumanReadablePart
def data: Vector[UInt5] def data: Vector[UInt5]
override def networkParameters = hrp.network.get override def networkParameters: NetworkParameters = hrp.network
override def value: String = { override def value: String = {
val all: Vector[UInt5] = data ++ checksum val all: Vector[UInt5] = data ++ checksum
@ -98,13 +97,17 @@ sealed abstract class Bech32Address extends BitcoinAddress {
} }
} }
override def toString = "Bech32Address(" + value + ")" def expandHrp: Vector[UInt5] = {
Bech32.hrpExpand(hrp)
}
override def toString: String = "Bech32Address(" + value + ")"
} }
object Bech32Address extends AddressFactory[Bech32Address] { object Bech32Address extends AddressFactory[Bech32Address] {
private case class Bech32AddressImpl( private case class Bech32AddressImpl(
hrp: HumanReadablePart, hrp: BtcHumanReadablePart,
data: Vector[UInt5]) data: Vector[UInt5])
extends Bech32Address { extends Bech32Address {
//require(verifyChecksum(hrp, data), "checksum did not pass") //require(verifyChecksum(hrp, data), "checksum did not pass")
@ -117,35 +120,26 @@ object Bech32Address extends AddressFactory[Bech32Address] {
val prog = UInt8.toUInt8s(witSPK.asmBytes.tail.tail) val prog = UInt8.toUInt8s(witSPK.asmBytes.tail.tail)
val encoded = Bech32.from8bitTo5bit(prog) val encoded = Bech32.from8bitTo5bit(prog)
val hrp = networkParameters match { val hrp = networkParameters match {
case _: MainNet => bc case _: MainNet => BtcHumanReadablePart.bc
case _: TestNet3 | _: RegTest => tb case _: TestNet3 => BtcHumanReadablePart.tb
case _: RegTest => BtcHumanReadablePart.bcrt
} }
val witVersion = witSPK.witnessVersion.version.toInt.toByte val witVersion = witSPK.witnessVersion.version.toInt.toByte
Bech32Address(hrp, Vector(UInt5(witVersion)) ++ encoded) Bech32Address(hrp, Vector(UInt5(witVersion)) ++ encoded)
} }
def apply(hrp: HumanReadablePart, data: Vector[UInt5]): Bech32Address = { def apply(hrp: BtcHumanReadablePart, data: Vector[UInt5]): Bech32Address = {
Bech32AddressImpl(hrp, data) Bech32AddressImpl(hrp, data)
} }
/** Returns a base 5 checksum as specified by BIP173 */ /** Returns a base 5 checksum as specified by BIP173 */
def createChecksum( def createChecksum(
hrp: HumanReadablePart, hrp: BtcHumanReadablePart,
bytes: Vector[UInt5]): Vector[UInt5] = { bytes: Vector[UInt5]): Vector[UInt5] = {
val values = hrpExpand(hrp) ++ bytes val values = Bech32.hrpExpand(hrp) ++ bytes
Bech32.createChecksum(values) Bech32.createChecksum(values)
} }
def hrpExpand(hrp: HumanReadablePart): Vector[UInt5] = {
Bech32.hrpExpand(hrp.bytes)
}
def verifyChecksum(hrp: HumanReadablePart, u5s: Seq[UInt5]): Boolean = {
val data = hrpExpand(hrp) ++ u5s
val checksum = Bech32.polyMod(data)
checksum == 1
}
/** Tries to convert the given string a to a /** Tries to convert the given string a to a
* [[org.bitcoins.core.protocol.script.WitnessScriptPubKey WitnessScriptPubKey]] */ * [[org.bitcoins.core.protocol.script.WitnessScriptPubKey WitnessScriptPubKey]] */
def fromStringToWitSPK(string: String): Try[WitnessScriptPubKey] = { def fromStringToWitSPK(string: String): Try[WitnessScriptPubKey] = {
@ -178,43 +172,12 @@ object Bech32Address extends AddressFactory[Bech32Address] {
} }
} }
/** Decodes bech32 string to the [[org.bitcoins.core.protocol.HumanReadablePart HumanReadablePart]] & data part */ /** Decodes bech32 string to the [[org.bitcoins.core.protocol.BtcHumanReadablePart HumanReadablePart]] & data part */
override def fromString(str: String): Try[Bech32Address] = { override def fromString(bech32: String): Try[Bech32Address] = {
val sepIndexes = str.zipWithIndex.filter(_._1 == Bech32.separator) for {
if (str.size > 90 || str.size < 8) { (hrp, data) <- Bech32.splitToHrpAndData(bech32)
Failure( btcHrp <- BtcHumanReadablePart(hrp)
new IllegalArgumentException( } yield Bech32Address(btcHrp, data)
"bech32 payloads must be betwee 8 and 90 chars, got: " + str.size))
} else if (sepIndexes.isEmpty) {
Failure(
new IllegalArgumentException(
"Bech32 address did not have the correct separator"))
} else {
val sepIndex = sepIndexes.last._2
val (hrp, data) = (str.take(sepIndex), str.splitAt(sepIndex + 1)._2)
if (hrp.size < 1 || data.size < 6) {
Failure(new IllegalArgumentException("Hrp/data too short"))
} else {
val hrpValid = checkHrpValidity(hrp)
val dataValid = Bech32.checkDataValidity(data)
val isChecksumValid: Try[Vector[UInt5]] = hrpValid.flatMap {
h: HumanReadablePart =>
dataValid.flatMap { d: Vector[UInt5] =>
if (verifyChecksum(h, d)) {
if (d.size < 6) Success(Vector.empty)
else Success(d.take(d.size - 6))
} else
Failure(
new IllegalArgumentException(
"Checksum was invalid on the bech32 address"))
}
}
isChecksumValid.flatMap { d: Vector[UInt5] =>
hrpValid.map(h => Bech32Address(h, d))
}
}
}
} }
override def fromScriptPubKey( override def fromScriptPubKey(
@ -232,42 +195,6 @@ object Bech32Address extends AddressFactory[Bech32Address] {
"Cannot create a address for the scriptPubKey: " + x)) "Cannot create a address for the scriptPubKey: " + x))
} }
/** Checks if the possible human readable part follows BIP173 rules */
private def checkHrpValidity(hrp: String): Try[HumanReadablePart] = {
@tailrec
def loop(
remaining: List[Char],
accum: Seq[UInt8],
isLower: Boolean,
isUpper: Boolean): Try[Seq[UInt8]] = remaining match {
case h :: t =>
if (h < 33 || h > 126) {
Failure(
new IllegalArgumentException(
"Invalid character range for hrp, got: " + hrp))
} else if (isLower && isUpper) {
Failure(
new IllegalArgumentException("HRP had mixed case, got: " + hrp))
} else {
loop(t,
UInt8(h.toByte) +: accum,
h.isLower || isLower,
h.isUpper || isUpper)
}
case Nil =>
if (isLower && isUpper) {
Failure(
new IllegalArgumentException("HRP had mixed case, got: " + hrp))
} else {
Success(accum.reverse)
}
}
loop(hrp.toCharArray.toList, Nil, false, false).flatMap { _ =>
Success(HumanReadablePart(hrp.toLowerCase))
}
}
} }
object P2PKHAddress extends AddressFactory[P2PKHAddress] { object P2PKHAddress extends AddressFactory[P2PKHAddress] {
@ -414,23 +341,12 @@ object BitcoinAddress extends AddressFactory[BitcoinAddress] {
def apply(value: String): Try[BitcoinAddress] = fromString(value) def apply(value: String): Try[BitcoinAddress] = fromString(value)
override def fromString(value: String): Try[BitcoinAddress] = { override def fromString(value: String): Try[BitcoinAddress] = {
val p2pkhTry = P2PKHAddress.fromString(value) P2PKHAddress
if (p2pkhTry.isSuccess) { .fromString(value)
p2pkhTry .orElse(P2SHAddress.fromString(value))
} else { .orElse(Bech32Address.fromString(value))
val p2shTry = P2SHAddress.fromString(value) .orElse(Failure(new IllegalArgumentException(
if (p2shTry.isSuccess) { s"Could not decode the given value to a BitcoinAddress, got: $value")))
p2shTry
} else {
val bech32Try = Bech32Address.fromString(value)
if (bech32Try.isSuccess) {
bech32Try
} else {
Failure(new IllegalArgumentException(
s"Could not decode the given value to a BitcoinAddress, got: $value"))
}
}
}
} }
override def fromScriptPubKey( override def fromScriptPubKey(

View file

@ -0,0 +1,59 @@
package org.bitcoins.core.protocol
import org.bitcoins.core.config._
import org.bitcoins.core.util.Bech32HumanReadablePart
import scala.util.{Failure, Success, Try}
/**
* Represents the HumanReadablePart of a Bech32 address
* [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki]]
*/
sealed abstract class BtcHumanReadablePart extends Bech32HumanReadablePart {
def network: BitcoinNetwork
}
object BtcHumanReadablePart {
/** Represents the HumanReadablePart for a bitcoin mainnet bech32 address */
case object bc extends BtcHumanReadablePart {
override def network: MainNet.type = MainNet
override def chars = "bc"
}
/** Represents the HumanReadablePart for a bitcoin testnet bech32 address */
case object tb extends BtcHumanReadablePart {
override def network: TestNet3.type = TestNet3
override def chars = "tb"
}
/**
* Represents the HumanReadablePart for a bitcoin regtest bech32 address
*
* @see Regtest is not covered in the BIP. See
* [[https://github.com/bitcoin/bitcoin/issues/12314 this issue]]
* for more context.
*/
case object bcrt extends BtcHumanReadablePart {
override def network: RegTest.type = RegTest
override def chars: String = "bcrt"
}
def apply(str: String): Try[BtcHumanReadablePart] = str match {
case "bc" => Success(bc)
case "tb" => Success(tb)
case "bcrt" => Success(bcrt) // Bitcoin Core specific
case _ =>
Failure(
new IllegalArgumentException(s"Could not construct BTC HRP from $str"))
}
def apply(network: NetworkParameters): BtcHumanReadablePart = network match {
case _: MainNet => bc
case _: TestNet3 => tb
case _: RegTest => bcrt
}
def apply(hrp: Bech32HumanReadablePart): Try[BtcHumanReadablePart] =
BtcHumanReadablePart(hrp.chars)
}

View file

@ -3,13 +3,13 @@ package org.bitcoins.core.protocol.ln
import org.bitcoins.core.config.NetworkParameters import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.protocol.ln.LnParams._ import org.bitcoins.core.protocol.ln.LnParams._
import org.bitcoins.core.protocol.ln.currency.{LnCurrencyUnit, LnCurrencyUnits} import org.bitcoins.core.protocol.ln.currency.{LnCurrencyUnit, LnCurrencyUnits}
import org.bitcoins.core.util.Bech32 import org.bitcoins.core.util.Bech32HumanReadablePart
import scodec.bits.ByteVector import scodec.bits.ByteVector
import scala.util.matching.Regex import scala.util.matching.Regex
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
sealed abstract class LnHumanReadablePart { sealed abstract class LnHumanReadablePart extends Bech32HumanReadablePart {
require(amount.isEmpty || amount.get.toBigInt > 0, require(amount.isEmpty || amount.get.toBigInt > 0,
s"Invoice amount must be greater then 0, got $amount") s"Invoice amount must be greater then 0, got $amount")
require( require(
@ -20,22 +20,15 @@ sealed abstract class LnHumanReadablePart {
def amount: Option[LnCurrencyUnit] def amount: Option[LnCurrencyUnit]
def bytes: ByteVector = { override lazy val chars: String = {
network.invoicePrefix ++ amount val amountEncoded = amount.map(_.toEncodedString).getOrElse("")
.map(_.encodedBytes) network.invoicePrefix + amountEncoded
.getOrElse(ByteVector.empty)
} }
override def toString: String = { lazy val bytes: ByteVector =
val b = StringBuilder.newBuilder ByteVector.encodeAscii(chars).right.get
val prefixChars = network.invoicePrefix.toArray.map(_.toChar)
prefixChars.foreach(b.append)
val amt = amount.map(_.toEncodedString).getOrElse("") override lazy val toString: String = chars
b.append(amt)
b.toString()
}
} }
object LnHumanReadablePart { object LnHumanReadablePart {
@ -58,6 +51,9 @@ object LnHumanReadablePart {
def network: LnParams = LnBitcoinRegTest def network: LnParams = LnBitcoinRegTest
} }
/** Tries to construct a LN HRP with optional amount specified from the given string */
def apply(bech32: String): Try[LnHumanReadablePart] = fromString(bech32)
def apply(network: NetworkParameters): LnHumanReadablePart = { def apply(network: NetworkParameters): LnHumanReadablePart = {
val lnNetwork = LnParams.fromNetworkParameters(network) val lnNetwork = LnParams.fromNetworkParameters(network)
LnHumanReadablePart.fromLnParams(lnNetwork) LnHumanReadablePart.fromLnParams(lnNetwork)
@ -109,26 +105,21 @@ object LnHumanReadablePart {
* and * and
* [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Specification BIP173]] * [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Specification BIP173]]
*/ */
def fromString(input: String): Try[LnHumanReadablePart] = { def fromString(bech32: String): Try[LnHumanReadablePart] = {
val hrpIsValidT = Bech32.checkHrpValidity(input, parse)
hrpIsValidT
}
private def parse(input: String): Try[LnHumanReadablePart] = {
//Select all of the letters, until we hit a number, as the network //Select all of the letters, until we hit a number, as the network
val networkPattern: Regex = "^[a-z]*".r val networkPattern: Regex = "^[a-z]*".r
val networkStringOpt = networkPattern.findFirstIn(input) val networkStringOpt = networkPattern.findFirstIn(bech32)
val lnParamsOpt = networkStringOpt.flatMap(LnParams.fromPrefixString(_)) val lnParamsOpt = networkStringOpt.flatMap(LnParams.fromPrefixString)
if (lnParamsOpt.isEmpty) { if (lnParamsOpt.isEmpty) {
Failure( Failure(
new IllegalArgumentException( new IllegalArgumentException(
s"Could not parse a valid network prefix, got ${input}")) s"Could not parse a valid network prefix, got $bech32"))
} else { } else {
val lnParams = lnParamsOpt.get val lnParams = lnParamsOpt.get
val prefixSize = lnParams.invoicePrefix.size.toInt val prefixSize = lnParams.invoicePrefix.length
val amountString = input.slice(prefixSize, input.size) val amountString = bech32.slice(prefixSize, bech32.length)
val amount = LnCurrencyUnits.fromEncodedString(amountString).toOption val amount = LnCurrencyUnits.fromEncodedString(amountString).toOption
//If we are able to parse something as an amount, but are unable to convert it to a LnCurrencyUnit, we should fail. //If we are able to parse something as an amount, but are unable to convert it to a LnCurrencyUnit, we should fail.
@ -142,5 +133,4 @@ object LnHumanReadablePart {
} }
} }
} }
} }

View file

@ -16,8 +16,8 @@ sealed abstract class LnInvoice {
s"timestamp ${timestamp.toBigInt} < ${LnInvoice.MAX_TIMESTAMP}") s"timestamp ${timestamp.toBigInt} < ${LnInvoice.MAX_TIMESTAMP}")
require( require(
isValidSignature(), isValidSignature,
s"Did not receive a valid digital signature for the invoice ${toString}") s"Did not receive a valid digital signature for the invoice $toString")
def hrp: LnHumanReadablePart def hrp: LnHumanReadablePart
@ -92,7 +92,7 @@ sealed abstract class LnInvoice {
bech32 bech32
} }
def isValidSignature(): Boolean = { def isValidSignature: Boolean = {
Try(nodeId.pubKey.verify(sigHash, signature.signature)).getOrElse(false) Try(nodeId.pubKey.verify(sigHash, signature.signature)).getOrElse(false)
} }
@ -126,11 +126,8 @@ object LnInvoice extends BitcoinSLogger {
UInt64(decoded) UInt64(decoded)
} }
def hrpExpand(lnHumanReadablePart: LnHumanReadablePart): Vector[UInt5] = { def hrpExpand(lnHumanReadablePart: LnHumanReadablePart): Vector[UInt5] =
val bytes = lnHumanReadablePart.bytes lnHumanReadablePart.expand
val u5s = Bech32.hrpExpand(bytes)
u5s
}
def createChecksum( def createChecksum(
hrp: LnHumanReadablePart, hrp: LnHumanReadablePart,
@ -154,7 +151,7 @@ object LnInvoice extends BitcoinSLogger {
val MIN_LENGTH = TIMESTAMP_LEN + SIGNATURE_LEN val MIN_LENGTH = TIMESTAMP_LEN + SIGNATURE_LEN
if (data.length < MIN_LENGTH) { if (data.length < MIN_LENGTH) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
s"Cannot create invoice with data length less then ${MIN_LENGTH}, got ${data.length}") s"Cannot create invoice with data length less then $MIN_LENGTH, got ${data.length}")
} else { } else {
//first 35 bits is time stamp //first 35 bits is time stamp
val timestampU5s = data.take(TIMESTAMP_LEN) val timestampU5s = data.take(TIMESTAMP_LEN)
@ -180,18 +177,20 @@ object LnInvoice extends BitcoinSLogger {
def fromString(bech32String: String): Try[LnInvoice] = { def fromString(bech32String: String): Try[LnInvoice] = {
val sepIndexes = { val sepIndexes = {
bech32String.zipWithIndex.filter(_._1 == Bech32.separator) bech32String.zipWithIndex.filter {
case (sep, _) => sep == Bech32.separator
}
} }
if (sepIndexes.isEmpty) { if (sepIndexes.isEmpty) {
Failure( Failure(
new IllegalArgumentException( new IllegalArgumentException(
"LnInvoice did not have the correct separator")) "LnInvoice did not have the correct separator"))
} else { } else {
val sepIndex = sepIndexes.last._2 val (_, sepIndex) = sepIndexes.last
val hrp = bech32String.take(sepIndex) val hrp = bech32String.take(sepIndex)
val data = bech32String.splitAt(sepIndex + 1)._2 val (_, data) = bech32String.splitAt(sepIndex + 1)
if (hrp.length < 1) { if (hrp.length < 1) {
Failure(new IllegalArgumentException("HumanReadablePart is too short")) Failure(new IllegalArgumentException("HumanReadablePart is too short"))

View file

@ -2,7 +2,6 @@ package org.bitcoins.core.protocol.ln
import org.bitcoins.core.config.{MainNet, NetworkParameters, RegTest, TestNet3} import org.bitcoins.core.config.{MainNet, NetworkParameters, RegTest, TestNet3}
import org.bitcoins.core.protocol.blockchain.ChainParams import org.bitcoins.core.protocol.blockchain.ChainParams
import scodec.bits.ByteVector
sealed abstract class LnParams { sealed abstract class LnParams {
def chain: ChainParams = network.chainParams def chain: ChainParams = network.chainParams
@ -18,7 +17,7 @@ sealed abstract class LnParams {
* [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md BOLT11]] * [[https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md BOLT11]]
* for more details * for more details
*/ */
def invoicePrefix: ByteVector def invoicePrefix: String
} }
object LnParams { object LnParams {
@ -30,9 +29,7 @@ object LnParams {
override def lnPort = 9735 override def lnPort = 9735
override val invoicePrefix: ByteVector = { override val invoicePrefix: String = "lnbc"
ByteVector('l', 'n', 'b', 'c')
}
} }
case object LnBitcoinTestNet extends LnParams { case object LnBitcoinTestNet extends LnParams {
@ -42,9 +39,7 @@ object LnParams {
override def lnPort = 9735 override def lnPort = 9735
override val invoicePrefix: ByteVector = { override val invoicePrefix: String = "lntb"
ByteVector('l', 'n', 't', 'b')
}
} }
case object LnBitcoinRegTest extends LnParams { case object LnBitcoinRegTest extends LnParams {
@ -54,9 +49,7 @@ object LnParams {
override def lnPort = 9735 override def lnPort = 9735
override val invoicePrefix: ByteVector = { override val invoicePrefix: String = "lnbcrt"
ByteVector('l', 'n', 'b', 'c', 'r', 't')
}
} }
def fromNetworkParameters(np: NetworkParameters): LnParams = np match { def fromNetworkParameters(np: NetworkParameters): LnParams = np match {
@ -71,7 +64,7 @@ object LnParams {
private val prefixes: Map[String, LnParams] = { private val prefixes: Map[String, LnParams] = {
val vec: Vector[(String, LnParams)] = { val vec: Vector[(String, LnParams)] = {
allNetworks.map { network => allNetworks.map { network =>
(network.invoicePrefix.decodeAscii.right.get, network) (network.invoicePrefix, network)
} }
} }
vec.toMap vec.toMap

View file

@ -1,6 +1,8 @@
package org.bitcoins.core.util package org.bitcoins.core.util
import org.bitcoins.core.number.{UInt32, UInt5, UInt8} import org.bitcoins.core.number.{UInt5, UInt8}
import org.bitcoins.core.protocol.BtcHumanReadablePart
import org.bitcoins.core.protocol.ln.LnHumanReadablePart
import scodec.bits.{BitVector, ByteVector} import scodec.bits.{BitVector, ByteVector}
import scala.annotation.tailrec import scala.annotation.tailrec
@ -13,11 +15,8 @@ import scala.util.{Failure, Success, Try}
*/ */
sealed abstract class Bech32 { sealed abstract class Bech32 {
private val generators: Vector[Long] = Vector(UInt32("3b6a57b2").toLong, private val generators: Vector[Long] =
UInt32("26508e6d").toLong, Vector(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
UInt32("1ea119fa").toLong,
UInt32("3d4233dd").toLong,
UInt32("2a1462b3").toLong)
/** /**
* Creates a checksum for the given byte vector according to * Creates a checksum for the given byte vector according to
@ -42,18 +41,18 @@ sealed abstract class Bech32 {
* Expands the human readable part of a bech32 address as per * Expands the human readable part of a bech32 address as per
* [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 BIP173]] * [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 BIP173]]
*/ */
def hrpExpand(bytes: ByteVector): Vector[UInt5] = { def hrpExpand(hrp: Bech32HumanReadablePart): Vector[UInt5] = {
val x: ByteVector = bytes.map { b: Byte => val lowerchars = hrp.chars.toLowerCase
(b >> 5).toByte
}
val withZero: ByteVector = x ++ ByteVector.low(1)
val y: ByteVector = bytes.map { char => val x: Vector[UInt5] = lowerchars.map { c =>
(char & 0x1f).toByte UInt5(c >> 5)
} }.toVector
val result = withZero ++ y
UInt5.toUInt5s(result) val y: Vector[UInt5] = lowerchars.map { c =>
UInt5(c & 0x1f)
}.toVector
x ++ (UInt5.zero +: y)
} }
def polyMod(bytes: Vector[UInt5]): Long = { def polyMod(bytes: Vector[UInt5]): Long = {
@ -75,15 +74,15 @@ sealed abstract class Bech32 {
/** Checks if the possible human readable part follows /** Checks if the possible human readable part follows
* [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 BIP173]] * [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 BIP173]]
* rules */ * rules */
def checkHrpValidity[T](hrp: String, f: String => Try[T]): Try[T] = { def checkHrpValidity(hrp: String): Try[Bech32HumanReadablePart] = {
@tailrec @tailrec
def loop( def loop(
remaining: List[Char], remaining: List[Char],
accum: Seq[UInt8], accum: List[Char],
isLower: Boolean, isLower: Boolean,
isUpper: Boolean): Try[Seq[UInt8]] = remaining match { isUpper: Boolean): Try[Seq[Char]] = remaining match {
case h :: t => case h :: t =>
if (h < 33 || h > 126) { if (!isInHrpRange(h)) {
Failure( Failure(
new IllegalArgumentException( new IllegalArgumentException(
"Invalid character range for hrp, got: " + hrp)) "Invalid character range for hrp, got: " + hrp))
@ -91,10 +90,7 @@ sealed abstract class Bech32 {
Failure( Failure(
new IllegalArgumentException("HRP had mixed case, got: " + hrp)) new IllegalArgumentException("HRP had mixed case, got: " + hrp))
} else { } else {
loop(remaining = t, loop(t, h +: accum, h.isLower || isLower, h.isUpper || isUpper)
accum = UInt8(h.toByte) +: accum,
isLower = h.isLower || isLower,
isUpper = h.isUpper || isUpper)
} }
case Nil => case Nil =>
if (isLower && isUpper) { if (isLower && isUpper) {
@ -105,14 +101,23 @@ sealed abstract class Bech32 {
} }
} }
val isValid = val hrpT =
loop(hrp.toCharArray.toList, Nil, isLower = false, isUpper = false) loop(hrp.toCharArray.toList, Nil, isLower = false, isUpper = false)
isValid.flatMap { _ => hrpT.flatMap { chars =>
f(hrp.toLowerCase) val str = chars.mkString
val lnT = LnHumanReadablePart(str)
val btcT = BtcHumanReadablePart(str)
lnT
.orElse(btcT)
.orElse(Failure(new IllegalArgumentException(
s"Could not construct valid LN or BTC HRP from $str ")))
} }
} }
def isInHrpRange(char: Char): Boolean = char >= 33 && char <= 126
/** /**
* Takes in the data portion of a bech32 address and decodes it to a byte array * Takes in the data portion of a bech32 address and decodes it to a byte array
* It also checks the validity of the data portion according to BIP173 * It also checks the validity of the data portion according to BIP173
@ -226,13 +231,83 @@ sealed abstract class Bech32 {
NumberUtil.convertUInt5sToUInt8(b) NumberUtil.convertUInt5sToUInt8(b)
} }
/** Assumes we are given a valid bech32 string */ /**
def decodeStringToU5s(str: String): Vector[UInt5] = { * Validate a Bech32 string, and determine HRP and data.
str.map { char => * Fails if HRP is not LN or BTC compatible.
UInt5(Bech32.charset.indexOf(char)) *
}.toVector * @see Mimics
* [[https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py#L62 this function]]
* by Sipa
*/
def splitToHrpAndData(
bech32: String): Try[(Bech32HumanReadablePart, Vector[UInt5])] = {
val sepIndexes = bech32.zipWithIndex.filter {
case (sep, _) => sep == Bech32.separator
}
val length = bech32.length
val maxLength =
// is this a LN invoice or not?
if (bech32.startsWith("ln"))
// BOLT 11 is not fully bech32 compatible
// https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#requirements
Integer.MAX_VALUE
else
90
if (length > maxLength || length < 8) {
Failure(
new IllegalArgumentException(
"Bech32 payloads must be between 8 and 90 chars, got: " + length))
} else if (sepIndexes.isEmpty) {
Failure(
new IllegalArgumentException(
"Bech32 payload did not have the correct separator"))
} else {
val (_, sepIndex) = sepIndexes.last
val hrpStr = bech32.take(sepIndex)
val (_, dataStr) = bech32.splitAt(sepIndex + 1)
if (hrpStr.length < 1) {
Failure(new IllegalArgumentException("HRP too short"))
} else if (dataStr.length < 6) {
Failure(new IllegalArgumentException("Hrp/data too short"))
} else {
for {
hrp <- checkHrpValidity(hrpStr)
dataWithCheck <- Bech32.checkDataValidity(dataStr)
dataNoCheck <- {
if (verifyChecksum(hrp, dataWithCheck)) {
Success(dataWithCheck.take(dataWithCheck.size - 6))
} else
Failure(
new IllegalArgumentException(
s"Checksum was invalid on bech32 string $bech32"))
}
} yield (hrp, dataNoCheck)
}
}
} }
def verifyChecksum(hrp: Bech32HumanReadablePart, u5s: Seq[UInt5]): Boolean = {
val expandedHrp = hrpExpand(hrp)
val data = expandedHrp ++ u5s
val checksum = Bech32.polyMod(data)
checksum == 1
}
/** Assumes we are given a valid bech32 string */
def decodeStringToU5s(str: String): Vector[UInt5] = {
str
.map(_.toLower)
.map { char =>
val index = Bech32.charset.indexOf(char)
require(index > 0,
s"$char (${char.toInt}) is not part of the Bech32 charset!")
UInt5(index)
}
.toVector
}
} }
object Bech32 extends Bech32 { object Bech32 extends Bech32 {
@ -248,3 +323,13 @@ object Bech32 extends Bech32 {
'g', 'f', '2', 't', 'v', 'd', 'w', '0', 's', '3', 'j', 'n', '5', '4', 'k', 'g', 'f', '2', 't', 'v', 'd', 'w', '0', 's', '3', 'j', 'n', '5', '4', 'k',
'h', 'c', 'e', '6', 'm', 'u', 'a', '7', 'l') 'h', 'c', 'e', '6', 'm', 'u', 'a', '7', 'l')
} }
abstract class Bech32HumanReadablePart {
require(chars.forall(Bech32.isInHrpRange),
s"Some characters in $chars were not in valid HRP range ([33-126])")
def chars: String
/** Expands this HRP into a vector of UInt5s, in accordance with the Bech32 spec */
def expand: Vector[UInt5] = Bech32.hrpExpand(this)
}