diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/AddressTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/AddressTest.scala index 43293eb5b8..212bfcd61c 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/AddressTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/AddressTest.scala @@ -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) + } + } + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Spec.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Spec.scala index 559aae7817..5103da4362 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Spec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Spec.scala @@ -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) diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Test.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Test.scala index fd8e5c59d6..68313f71ac 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Test.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/Bech32Test.scala @@ -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 diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/BitcoinAddressTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/BitcoinAddressTest.scala index 8abfb4a747..c4f8b3fb91 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/BitcoinAddressTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/BitcoinAddressTest.scala @@ -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) diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/BtcHumanReadablePartTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/BtcHumanReadablePartTest.scala new file mode 100644 index 0000000000..6eb321ee87 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/BtcHumanReadablePartTest.scala @@ -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) + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnInvoiceUnitTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnInvoiceUnitTest.scala index f200c8d98f..7caffb1033 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnInvoiceUnitTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/ln/LnInvoiceUnitTest.scala @@ -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") diff --git a/core/src/main/scala/org/bitcoins/core/protocol/Address.scala b/core/src/main/scala/org/bitcoins/core/protocol/Address.scala index 1b298d0471..3f58cc3581 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/Address.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/Address.scala @@ -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( diff --git a/core/src/main/scala/org/bitcoins/core/protocol/BtcHumanReadablePart.scala b/core/src/main/scala/org/bitcoins/core/protocol/BtcHumanReadablePart.scala new file mode 100644 index 0000000000..3eea187148 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/BtcHumanReadablePart.scala @@ -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) +} diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePart.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePart.scala index d6989b3073..1fe4bce719 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePart.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnHumanReadablePart.scala @@ -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 { } } } - } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoice.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoice.scala index 645b9d1938..f364a127e2 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoice.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnInvoice.scala @@ -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")) diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnParams.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnParams.scala index 41a9fd2215..027f4396d6 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnParams.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnParams.scala @@ -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 diff --git a/core/src/main/scala/org/bitcoins/core/util/Bech32.scala b/core/src/main/scala/org/bitcoins/core/util/Bech32.scala index 65d765ac8d..93dc6bd96f 100644 --- a/core/src/main/scala/org/bitcoins/core/util/Bech32.scala +++ b/core/src/main/scala/org/bitcoins/core/util/Bech32.scala @@ -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) +}