mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-23 06:45:21 +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
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 =
|
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")
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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.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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue