mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-22 14:33:06 +01:00
Rework Bech32 (#360)
Refactor commonalities between LN and BTC Add support for RegTest
This commit is contained in:
parent
bad37db6fa
commit
25fa009b95
12 changed files with 456 additions and 265 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue