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
import org.scalatest.{FlatSpec, MustMatchers}
import org.bitcoins.core.util.BitcoinSUnitTest
import org.bitcoins.testkit.core.gen.AddressGenerator
/**
* Created by chris on 3/23/15.
*/
class AddressTest extends FlatSpec with MustMatchers {}
import scala.util.{Failure, Success}
class AddressTest extends BitcoinSUnitTest {
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
import org.bitcoins.core.util.Bech32
import org.bitcoins.testkit.core.gen.ln.LnInvoiceGen
import org.bitcoins.testkit.core.gen.{
AddressGenerator,
ChainParamsGenerator,
ScriptGenerators
}
import org.bitcoins.core.util.{Bech32, BitcoinSLogger}
import org.scalacheck.{Prop, Properties}
import scala.annotation.tailrec
import scala.util.{Random, Success}
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") = {
Prop.forAll(ScriptGenerators.witnessScriptPubKey,
@ -25,27 +38,25 @@ class Bech32Spec extends Properties("Bech32Spec") {
}
property("checksum must not work if we modify a char") = {
Prop.forAll(AddressGenerator.bech32Address) {
case addr: Bech32Address =>
val old = addr.value
val rand = Math.abs(Random.nextInt)
val idx = rand % old.size
val (f, l) = old.splitAt(idx)
val replacementChar = pickReplacementChar(l.head)
val replaced = f ++ Seq(replacementChar) ++ l.tail
//should fail because we replaced a char in the addr, so checksum invalid
Bech32Address.fromString(replaced).isFailure
Prop.forAll(AddressGenerator.bech32Address) { addr: Bech32Address =>
val old = addr.value
val rand = Math.abs(Random.nextInt)
val idx = rand % old.length
val (f, l) = old.splitAt(idx)
val replacementChar = pickReplacementChar(l.head)
val replaced = f ++ Seq(replacementChar) ++ l.tail
//should fail because we replaced a char in the addr, so checksum invalid
Bech32Address.fromString(replaced).isFailure
}
}
property("must fail if we have a mixed case") = {
Prop.forAllNoShrink(AddressGenerator.bech32Address) {
case addr: Bech32Address =>
val old = addr.value
val replaced = switchCaseRandChar(old)
//should fail because we we switched the case of a random char
val actual = Bech32Address.fromString(replaced)
actual.isFailure
Prop.forAllNoShrink(AddressGenerator.bech32Address) { addr: Bech32Address =>
val old = addr.value
val replaced = switchCaseRandChar(old)
//should fail because we we switched the case of a random char
val actual = Bech32Address.fromString(replaced)
actual.isFailure
}
}
@ -61,7 +72,7 @@ class Bech32Spec extends Properties("Bech32Spec") {
@tailrec
private def switchCaseRandChar(addr: String): String = {
val rand = Math.abs(Random.nextInt)
val idx = rand % addr.size
val idx = rand % addr.length
val (f, l) = addr.splitAt(idx)
if (l.head.isDigit) {
switchCaseRandChar(addr)

View file

@ -2,49 +2,28 @@ package org.bitcoins.core.protocol
import org.bitcoins.core.config.{MainNet, TestNet3}
import org.bitcoins.core.crypto.ECPublicKey
import org.bitcoins.testkit.core.gen.NumberGenerator
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.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 {
override implicit val generatorDrivenConfig = generatorDrivenConfigNewCode
"Bech32" must "validly encode the test vectors from bitcoin core correctly" in {
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)
}
override implicit val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
it must "mark invalid test vectors as invalid from bitcoin core" in {
val invalid = Seq(
" 1nwldj5",
"\\x7f\"\"1axkwrx",
"\\x80\"\"1eym55h",
"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx",
"pzry9x0s0muk",
"1pzry9x0s0muk",
"x1b4n0q5v",
"li1dgmt3",
"de1lg7wt\\xff",
"A1G7SGD8",
"10a06t8",
"1qzzfhee"
)
val results: Seq[Try[Bech32Address]] =
invalid.map(Bech32Address.fromString(_))
results.exists(_.isSuccess) must be(false)
behavior of "Bech32"
it must "decode a regtest address from Bitcoin Core" in {
val addrStr = "bcrt1qq6w6pu6zq90az9krn53zlkvgyzkyeglzukyepf"
val addrT = Address.fromString(addrStr)
addrT match {
case Success(addr: Bech32Address) => assert(addr.value == addrStr)
case _ => fail()
}
}
it must "follow the example in BIP173" in {
@ -82,21 +61,103 @@ class Bech32Test extends BitcoinSUnitTest {
mp2wshDecoded must be(Success(p2wsh))
}
it must "expand the human readable part correctly" in {
Bech32Address.hrpExpand(bc) must be(
it must "expand the human readable part correctly - BTC" in {
BtcHumanReadablePart.bc.expand must be(
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)))
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 {
val addr = Bech32Address(bc, Vector(UInt5.zero))
val addr = Bech32Address(BtcHumanReadablePart.bc, Vector(UInt5.zero))
addr.value must be("bc1q9zpgru")
}
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.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))
encoded1 must be(Seq(fz, fz, fz, UInt5(16.toByte)))
//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)))
//130.toByte == -126

View file

@ -1,11 +1,11 @@
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.protocol.script.ScriptPubKey
import org.scalatest.{FlatSpec, MustMatchers}
import scala.util.Try
import scala.util.{Failure, Success, Try}
class BitcoinAddressTest extends FlatSpec with MustMatchers {
@ -27,10 +27,44 @@ class BitcoinAddressTest extends FlatSpec with MustMatchers {
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 {
BitcoinAddress.fromString("").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 {
val address = "3J98t1WpEZ73CNmQviecrnyiW"
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 =
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 {
@ -439,7 +439,7 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
invoice.lnTags.expiryTime.get.u32 must be(UInt32(3600))
invoice.isValidSignature() must be(true)
invoice.isValidSignature must be(true)
invoice.signatureData.toHex must be(
"6c6e74623130306e0b851aec410d1ae02e6dcc82f0480d4d39e3e1ada8ca5170d6ad19391d7b403a4b1ee61d57e741a72bd91323ab930ba34b7b7111d1898181818161131b430b73732b6111d113a3930b232b991161132bb32b73a111d1139bab139b1b934b1329116113abab4b2111d111818189899191999969a1a1a9a969b1b1b9b969c1c1c9c96b0b0b13131b1b23232b2b33311161132bc31b430b733b2911d113134ba3334b732bc11161139bcb6b137b6111d11212a21aaa9a2113e8c018e100")

View file

@ -1,5 +1,5 @@
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.number.{UInt5, UInt8}
import org.bitcoins.core.protocol.script._
@ -7,7 +7,6 @@ import org.bitcoins.core.script.constant.ScriptConstant
import org.bitcoins.core.util._
import scodec.bits.ByteVector
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
sealed abstract class Address {
@ -66,11 +65,11 @@ sealed abstract class P2SHAddress extends BitcoinAddress {
*/
sealed abstract class Bech32Address extends BitcoinAddress {
def hrp: HumanReadablePart
def hrp: BtcHumanReadablePart
def data: Vector[UInt5]
override def networkParameters = hrp.network.get
override def networkParameters: NetworkParameters = hrp.network
override def value: String = {
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] {
private case class Bech32AddressImpl(
hrp: HumanReadablePart,
hrp: BtcHumanReadablePart,
data: Vector[UInt5])
extends Bech32Address {
//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 encoded = Bech32.from8bitTo5bit(prog)
val hrp = networkParameters match {
case _: MainNet => bc
case _: TestNet3 | _: RegTest => tb
case _: MainNet => BtcHumanReadablePart.bc
case _: TestNet3 => BtcHumanReadablePart.tb
case _: RegTest => BtcHumanReadablePart.bcrt
}
val witVersion = witSPK.witnessVersion.version.toInt.toByte
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)
}
/** Returns a base 5 checksum as specified by BIP173 */
def createChecksum(
hrp: HumanReadablePart,
hrp: BtcHumanReadablePart,
bytes: Vector[UInt5]): Vector[UInt5] = {
val values = hrpExpand(hrp) ++ bytes
val values = Bech32.hrpExpand(hrp) ++ bytes
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
* [[org.bitcoins.core.protocol.script.WitnessScriptPubKey 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 */
override def fromString(str: String): Try[Bech32Address] = {
val sepIndexes = str.zipWithIndex.filter(_._1 == Bech32.separator)
if (str.size > 90 || str.size < 8) {
Failure(
new IllegalArgumentException(
"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))
}
}
}
/** Decodes bech32 string to the [[org.bitcoins.core.protocol.BtcHumanReadablePart HumanReadablePart]] & data part */
override def fromString(bech32: String): Try[Bech32Address] = {
for {
(hrp, data) <- Bech32.splitToHrpAndData(bech32)
btcHrp <- BtcHumanReadablePart(hrp)
} yield Bech32Address(btcHrp, data)
}
override def fromScriptPubKey(
@ -232,42 +195,6 @@ object Bech32Address extends AddressFactory[Bech32Address] {
"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] {
@ -414,23 +341,12 @@ object BitcoinAddress extends AddressFactory[BitcoinAddress] {
def apply(value: String): Try[BitcoinAddress] = fromString(value)
override def fromString(value: String): Try[BitcoinAddress] = {
val p2pkhTry = P2PKHAddress.fromString(value)
if (p2pkhTry.isSuccess) {
p2pkhTry
} else {
val p2shTry = P2SHAddress.fromString(value)
if (p2shTry.isSuccess) {
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"))
}
}
}
P2PKHAddress
.fromString(value)
.orElse(P2SHAddress.fromString(value))
.orElse(Bech32Address.fromString(value))
.orElse(Failure(new IllegalArgumentException(
s"Could not decode the given value to a BitcoinAddress, got: $value")))
}
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.protocol.ln.LnParams._
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 scala.util.matching.Regex
import scala.util.{Failure, Success, Try}
sealed abstract class LnHumanReadablePart {
sealed abstract class LnHumanReadablePart extends Bech32HumanReadablePart {
require(amount.isEmpty || amount.get.toBigInt > 0,
s"Invoice amount must be greater then 0, got $amount")
require(
@ -20,22 +20,15 @@ sealed abstract class LnHumanReadablePart {
def amount: Option[LnCurrencyUnit]
def bytes: ByteVector = {
network.invoicePrefix ++ amount
.map(_.encodedBytes)
.getOrElse(ByteVector.empty)
override lazy val chars: String = {
val amountEncoded = amount.map(_.toEncodedString).getOrElse("")
network.invoicePrefix + amountEncoded
}
override def toString: String = {
val b = StringBuilder.newBuilder
val prefixChars = network.invoicePrefix.toArray.map(_.toChar)
prefixChars.foreach(b.append)
lazy val bytes: ByteVector =
ByteVector.encodeAscii(chars).right.get
val amt = amount.map(_.toEncodedString).getOrElse("")
b.append(amt)
b.toString()
}
override lazy val toString: String = chars
}
object LnHumanReadablePart {
@ -58,6 +51,9 @@ object LnHumanReadablePart {
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 = {
val lnNetwork = LnParams.fromNetworkParameters(network)
LnHumanReadablePart.fromLnParams(lnNetwork)
@ -109,26 +105,21 @@ object LnHumanReadablePart {
* and
* [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Specification BIP173]]
*/
def fromString(input: String): Try[LnHumanReadablePart] = {
val hrpIsValidT = Bech32.checkHrpValidity(input, parse)
hrpIsValidT
}
private def parse(input: String): Try[LnHumanReadablePart] = {
def fromString(bech32: String): Try[LnHumanReadablePart] = {
//Select all of the letters, until we hit a number, as the network
val networkPattern: Regex = "^[a-z]*".r
val networkStringOpt = networkPattern.findFirstIn(input)
val lnParamsOpt = networkStringOpt.flatMap(LnParams.fromPrefixString(_))
val networkStringOpt = networkPattern.findFirstIn(bech32)
val lnParamsOpt = networkStringOpt.flatMap(LnParams.fromPrefixString)
if (lnParamsOpt.isEmpty) {
Failure(
new IllegalArgumentException(
s"Could not parse a valid network prefix, got ${input}"))
s"Could not parse a valid network prefix, got $bech32"))
} else {
val lnParams = lnParamsOpt.get
val prefixSize = lnParams.invoicePrefix.size.toInt
val amountString = input.slice(prefixSize, input.size)
val prefixSize = lnParams.invoicePrefix.length
val amountString = bech32.slice(prefixSize, bech32.length)
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.
@ -142,5 +133,4 @@ object LnHumanReadablePart {
}
}
}
}

View file

@ -16,8 +16,8 @@ sealed abstract class LnInvoice {
s"timestamp ${timestamp.toBigInt} < ${LnInvoice.MAX_TIMESTAMP}")
require(
isValidSignature(),
s"Did not receive a valid digital signature for the invoice ${toString}")
isValidSignature,
s"Did not receive a valid digital signature for the invoice $toString")
def hrp: LnHumanReadablePart
@ -92,7 +92,7 @@ sealed abstract class LnInvoice {
bech32
}
def isValidSignature(): Boolean = {
def isValidSignature: Boolean = {
Try(nodeId.pubKey.verify(sigHash, signature.signature)).getOrElse(false)
}
@ -126,11 +126,8 @@ object LnInvoice extends BitcoinSLogger {
UInt64(decoded)
}
def hrpExpand(lnHumanReadablePart: LnHumanReadablePart): Vector[UInt5] = {
val bytes = lnHumanReadablePart.bytes
val u5s = Bech32.hrpExpand(bytes)
u5s
}
def hrpExpand(lnHumanReadablePart: LnHumanReadablePart): Vector[UInt5] =
lnHumanReadablePart.expand
def createChecksum(
hrp: LnHumanReadablePart,
@ -154,7 +151,7 @@ object LnInvoice extends BitcoinSLogger {
val MIN_LENGTH = TIMESTAMP_LEN + SIGNATURE_LEN
if (data.length < MIN_LENGTH) {
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 {
//first 35 bits is time stamp
val timestampU5s = data.take(TIMESTAMP_LEN)
@ -180,18 +177,20 @@ object LnInvoice extends BitcoinSLogger {
def fromString(bech32String: String): Try[LnInvoice] = {
val sepIndexes = {
bech32String.zipWithIndex.filter(_._1 == Bech32.separator)
bech32String.zipWithIndex.filter {
case (sep, _) => sep == Bech32.separator
}
}
if (sepIndexes.isEmpty) {
Failure(
new IllegalArgumentException(
"LnInvoice did not have the correct separator"))
} else {
val sepIndex = sepIndexes.last._2
val (_, sepIndex) = sepIndexes.last
val hrp = bech32String.take(sepIndex)
val data = bech32String.splitAt(sepIndex + 1)._2
val (_, data) = bech32String.splitAt(sepIndex + 1)
if (hrp.length < 1) {
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.protocol.blockchain.ChainParams
import scodec.bits.ByteVector
sealed abstract class LnParams {
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]]
* for more details
*/
def invoicePrefix: ByteVector
def invoicePrefix: String
}
object LnParams {
@ -30,9 +29,7 @@ object LnParams {
override def lnPort = 9735
override val invoicePrefix: ByteVector = {
ByteVector('l', 'n', 'b', 'c')
}
override val invoicePrefix: String = "lnbc"
}
case object LnBitcoinTestNet extends LnParams {
@ -42,9 +39,7 @@ object LnParams {
override def lnPort = 9735
override val invoicePrefix: ByteVector = {
ByteVector('l', 'n', 't', 'b')
}
override val invoicePrefix: String = "lntb"
}
case object LnBitcoinRegTest extends LnParams {
@ -54,9 +49,7 @@ object LnParams {
override def lnPort = 9735
override val invoicePrefix: ByteVector = {
ByteVector('l', 'n', 'b', 'c', 'r', 't')
}
override val invoicePrefix: String = "lnbcrt"
}
def fromNetworkParameters(np: NetworkParameters): LnParams = np match {
@ -71,7 +64,7 @@ object LnParams {
private val prefixes: Map[String, LnParams] = {
val vec: Vector[(String, LnParams)] = {
allNetworks.map { network =>
(network.invoicePrefix.decodeAscii.right.get, network)
(network.invoicePrefix, network)
}
}
vec.toMap

View file

@ -1,6 +1,8 @@
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 scala.annotation.tailrec
@ -13,11 +15,8 @@ import scala.util.{Failure, Success, Try}
*/
sealed abstract class Bech32 {
private val generators: Vector[Long] = Vector(UInt32("3b6a57b2").toLong,
UInt32("26508e6d").toLong,
UInt32("1ea119fa").toLong,
UInt32("3d4233dd").toLong,
UInt32("2a1462b3").toLong)
private val generators: Vector[Long] =
Vector(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
/**
* 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
* [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 BIP173]]
*/
def hrpExpand(bytes: ByteVector): Vector[UInt5] = {
val x: ByteVector = bytes.map { b: Byte =>
(b >> 5).toByte
}
val withZero: ByteVector = x ++ ByteVector.low(1)
def hrpExpand(hrp: Bech32HumanReadablePart): Vector[UInt5] = {
val lowerchars = hrp.chars.toLowerCase
val y: ByteVector = bytes.map { char =>
(char & 0x1f).toByte
}
val result = withZero ++ y
val x: Vector[UInt5] = lowerchars.map { c =>
UInt5(c >> 5)
}.toVector
UInt5.toUInt5s(result)
val y: Vector[UInt5] = lowerchars.map { c =>
UInt5(c & 0x1f)
}.toVector
x ++ (UInt5.zero +: y)
}
def polyMod(bytes: Vector[UInt5]): Long = {
@ -75,15 +74,15 @@ sealed abstract class Bech32 {
/** Checks if the possible human readable part follows
* [[https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 BIP173]]
* rules */
def checkHrpValidity[T](hrp: String, f: String => Try[T]): Try[T] = {
def checkHrpValidity(hrp: String): Try[Bech32HumanReadablePart] = {
@tailrec
def loop(
remaining: List[Char],
accum: Seq[UInt8],
accum: List[Char],
isLower: Boolean,
isUpper: Boolean): Try[Seq[UInt8]] = remaining match {
isUpper: Boolean): Try[Seq[Char]] = remaining match {
case h :: t =>
if (h < 33 || h > 126) {
if (!isInHrpRange(h)) {
Failure(
new IllegalArgumentException(
"Invalid character range for hrp, got: " + hrp))
@ -91,10 +90,7 @@ sealed abstract class Bech32 {
Failure(
new IllegalArgumentException("HRP had mixed case, got: " + hrp))
} else {
loop(remaining = t,
accum = UInt8(h.toByte) +: accum,
isLower = h.isLower || isLower,
isUpper = h.isUpper || isUpper)
loop(t, h +: accum, h.isLower || isLower, h.isUpper || isUpper)
}
case Nil =>
if (isLower && isUpper) {
@ -105,14 +101,23 @@ sealed abstract class Bech32 {
}
}
val isValid =
val hrpT =
loop(hrp.toCharArray.toList, Nil, isLower = false, isUpper = false)
isValid.flatMap { _ =>
f(hrp.toLowerCase)
hrpT.flatMap { chars =>
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
* It also checks the validity of the data portion according to BIP173
@ -226,13 +231,83 @@ sealed abstract class Bech32 {
NumberUtil.convertUInt5sToUInt8(b)
}
/** Assumes we are given a valid bech32 string */
def decodeStringToU5s(str: String): Vector[UInt5] = {
str.map { char =>
UInt5(Bech32.charset.indexOf(char))
}.toVector
/**
* Validate a Bech32 string, and determine HRP and data.
* Fails if HRP is not LN or BTC compatible.
*
* @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 {
@ -248,3 +323,13 @@ object Bech32 extends Bech32 {
'g', 'f', '2', 't', 'v', 'd', 'w', '0', 's', '3', 'j', 'n', '5', '4', 'k',
'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)
}