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 a4a20c9959..072a121521 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 @@ -1,7 +1,6 @@ package org.bitcoins.core.protocol.ln import org.bitcoins.core.crypto._ -import org.bitcoins.testkit.core.gen.ln.LnInvoiceGen import org.bitcoins.core.number.{UInt32, UInt64, UInt8} import org.bitcoins.core.protocol.ln.LnParams.{ LnBitcoinMainNet, @@ -19,6 +18,7 @@ import org.bitcoins.core.protocol.ln.fee.{ import org.bitcoins.core.protocol.ln.routing.LnRoute import org.bitcoins.core.protocol.{Bech32Address, P2PKHAddress, P2SHAddress} import org.bitcoins.core.util.CryptoUtil +import org.bitcoins.testkit.core.gen.ln.LnInvoiceGen import org.bitcoins.testkit.util.BitcoinSUnitTest import scodec.bits.ByteVector @@ -113,13 +113,17 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest { val descriptionTagE = Left(LnTag.DescriptionTag("ナンセンス 1杯")) val expiryTag = LnTag.ExpiryTimeTag(UInt32(60)) - val lnTags = LnTaggedFields(paymentTag, - descriptionTagE, - None, - Some(expiryTag), - None, - None, - None) + val lnTags = LnTaggedFields( + paymentHash = paymentTag, + secret = None, + descriptionOrHash = descriptionTagE, + nodeId = None, + expiryTime = Some(expiryTag), + cltvExpiry = None, + fallbackAddress = None, + routingInfo = None, + features = None + ) val signature = ECDigitalSignature.fromRS( "259f04511e7ef2aa77f6ff04d51b4ae9209504843e5ab9672ce32a153681f687515b73ce57ee309db588a10eb8e41b5a2d2bc17144ddf398033faa49ffe95ae6") @@ -359,6 +363,45 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest { deserialized.get must be(lnInvoice) } + it must "parse BOLT11 example 10" in { + val expected = + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqqq4u9s93jtgysm3mrwll70zr697y3mf902hvxwej0v7c62rsltw83ng0pu8w3j230sluc5gxkdmm9dvpy9y6ggtjd2w544mzdrcs42t7sqdkcy8h" + + val signature = ECDigitalSignature.fromHex( + "3045022100af0b02c64b4121b8ec6efffcf10f45f123b495eabb0cecc9ecf634a1c3eb71e30220343c3c3ba32545f0ff31441acddecad60485269085c9aa752b5d89a3c42aa5fa") + val lnInvoiceSig = + LnInvoiceSignature(recoverId = UInt8.zero, signature = signature) + + val descriptionTag = Left(LnTag.DescriptionTag("coffee beans")) + + val paymentSecret = Some( + LnTag.SecretTag(PaymentSecret.fromHex( + "1111111111111111111111111111111111111111111111111111111111111111"))) + + val features = Some( + LnTag.FeaturesTag(ByteVector.fromValidHex("800000000000000000000800"))) + + val lnTags = LnTaggedFields(paymentHash = paymentTag, + descriptionOrHash = descriptionTag, + secret = paymentSecret, + features = features) + + val hrpMilli = + LnHumanReadablePart(LnBitcoinMainNet, Some(MilliBitcoins(25))) + + val lnInvoice = LnInvoice(hrp = hrpMilli, + timestamp = time, + lnTags = lnTags, + signature = lnInvoiceSig) + + val serialized = lnInvoice.toString + serialized must be(expected) + + val deserialized = LnInvoice.fromString(serialized) + + deserialized.get.toString must be(serialized) + } + it must "deserialize and reserialize a invoice with a explicity expiry time" in { //from eclair val bech32 = @@ -449,4 +492,16 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest { invoice.nodeId.hex must be(expected) } + + it must "parse secret and features tags" in { + // generated by Eclair 3.3.0-SNAPSHOT + val serialized = + "lnbcrt10n1p0px7lfpp5ghc2y7ttnwy58jx0dfcsdxy7ey0qfryn0wcmm04ckud0qw73kt9sdq9vehk7xqrrss9qypqqqsp5qlf6efygd26y03y66jdqqfmlxthplnu5cc8648fgn88twhpyvmgqg9k5kd0k8vv3xvvqpkhkt9chdl579maq45gvck4g0yd0eggmvfkzgvjmwn29r99p57tgyl3l3s82hlc4e97at55xl5lyzpfk6n36yyqqxeem8q" + val invoice = LnInvoice.fromString(serialized).get + invoice.lnTags.secret must be( + Some(LnTag.SecretTag(PaymentSecret.fromHex( + "07d3aca4886ab447c49ad49a00277f32ee1fcf94c60faa9d2899ceb75c2466d0")))) + invoice.lnTags.features must be( + Some(LnTag.FeaturesTag(ByteVector.fromValidHex("0800")))) + } } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTagPrefix.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTagPrefix.scala index e3394f99fc..017a93cef0 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTagPrefix.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTagPrefix.scala @@ -18,6 +18,9 @@ object LnTagPrefix { case object PaymentHash extends LnTagPrefix { override val value: Char = 'p' } + case object Secret extends LnTagPrefix { + override val value: Char = 's' + } case object Description extends LnTagPrefix { override val value: Char = 'd' } @@ -47,15 +50,21 @@ object LnTagPrefix { override val value: Char = 'r' } + case object Features extends LnTagPrefix { + override val value: Char = '9' + } + private lazy val all: Map[Char, LnTagPrefix] = List(PaymentHash, + Secret, Description, NodeId, DescriptionHash, ExpiryTime, CltvExpiry, FallbackAddress, - RoutingInfo) + RoutingInfo, + Features) .map(prefix => prefix.value -> prefix) .toMap diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTaggedFields.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTaggedFields.scala index 3c5ad1ca94..ea87e1517f 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTaggedFields.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTaggedFields.scala @@ -23,6 +23,8 @@ sealed abstract class LnTaggedFields extends NetworkElement { def paymentHash: LnTag.PaymentHashTag + def secret: Option[LnTag.SecretTag] + def description: Option[LnTag.DescriptionTag] def nodeId: Option[LnTag.NodeIdTag] @@ -37,14 +39,18 @@ sealed abstract class LnTaggedFields extends NetworkElement { def routingInfo: Option[LnTag.RoutingInfo] + def features: Option[LnTag.FeaturesTag] + lazy val data: Vector[UInt5] = Vector(Some(paymentHash), description, nodeId, descriptionHash, + secret, expiryTime, cltvExpiry, fallbackAddress, - routingInfo) + routingInfo, + features) .filter(_.isDefined) .flatMap(_.get.data) @@ -70,9 +76,11 @@ object LnTaggedFields { nodeId: Option[LnTag.NodeIdTag], descriptionHash: Option[LnTag.DescriptionHashTag], expiryTime: Option[LnTag.ExpiryTimeTag], + secret: Option[LnTag.SecretTag], cltvExpiry: Option[LnTag.MinFinalCltvExpiry], fallbackAddress: Option[LnTag.FallbackAddressTag], - routingInfo: Option[LnTag.RoutingInfo]) + routingInfo: Option[LnTag.RoutingInfo], + features: Option[LnTag.FeaturesTag]) extends LnTaggedFields /** @@ -82,11 +90,13 @@ object LnTaggedFields { def apply( paymentHash: LnTag.PaymentHashTag, descriptionOrHash: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag], + secret: Option[LnTag.SecretTag] = None, nodeId: Option[LnTag.NodeIdTag] = None, expiryTime: Option[LnTag.ExpiryTimeTag] = None, cltvExpiry: Option[LnTag.MinFinalCltvExpiry] = None, fallbackAddress: Option[LnTag.FallbackAddressTag] = None, - routingInfo: Option[LnTag.RoutingInfo] = None): LnTaggedFields = { + routingInfo: Option[LnTag.RoutingInfo] = None, + features: Option[LnTag.FeaturesTag] = None): LnTaggedFields = { val (description, descriptionHash): ( Option[LnTag.DescriptionTag], @@ -102,13 +112,15 @@ object LnTaggedFields { InvoiceTagImpl( paymentHash = paymentHash, + secret = secret, description = description, nodeId = nodeId, descriptionHash = descriptionHash, expiryTime = expiryTime, cltvExpiry = cltvExpiry, fallbackAddress = fallbackAddress, - routingInfo = routingInfo + routingInfo = routingInfo, + features = features ) } @@ -164,6 +176,8 @@ object LnTaggedFields { s"Payment hash must be defined in a LnInvoice") ) + val secret = getTag[LnTag.SecretTag] + val description = getTag[LnTag.DescriptionTag] val descriptionHash = getTag[LnTag.DescriptionHashTag] @@ -178,6 +192,8 @@ object LnTaggedFields { val routingInfo = getTag[LnTag.RoutingInfo] + val features = getTag[LnTag.FeaturesTag] + val d: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag] = { if (description.isDefined && descriptionHash.isDefined) { throw new IllegalArgumentException( @@ -194,12 +210,14 @@ object LnTaggedFields { LnTaggedFields( paymentHash = paymentHashTag, + secret = secret, descriptionOrHash = d, nodeId = nodeId, expiryTime = expiryTime, cltvExpiry = cltvExpiry, fallbackAddress = fallbackAddress, - routingInfo = routingInfo + routingInfo = routingInfo, + features = features ) } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTags.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTags.scala index d29b328a41..fbee87a5d8 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTags.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/LnTags.scala @@ -128,6 +128,15 @@ object LnTag { } } + case class SecretTag(secret: PaymentSecret) extends LnTag { + + override val prefix: LnTagPrefix = LnTagPrefix.Secret + + override val encoded: Vector[UInt5] = { + Bech32.from8bitTo5bit(secret.bytes) + } + } + case class DescriptionTag(string: String) extends LnTag { override val prefix: LnTagPrefix = LnTagPrefix.Description @@ -261,6 +270,9 @@ object LnTag { val hash = Sha256Digest.fromBytes(bytes) LnTag.PaymentHashTag(hash) + case LnTagPrefix.Secret => + LnTag.SecretTag(PaymentSecret.fromBytes(bytes)) + case LnTagPrefix.Description => val description = new String(bytes.toArray, Charset.forName("UTF-8")) LnTag.DescriptionTag(description) @@ -287,13 +299,26 @@ object LnTag { case LnTagPrefix.FallbackAddress => val version = payload.head.toUInt8 val noVersion = payload.tail - val noVersionBytes = UInt8.toBytes(Bech32.from5bitTo8bit(noVersion)) + val noVersionBytes = + UInt8.toBytes(Bech32.from5bitTo8bit(noVersion)) FallbackAddressV.fromU8(version, noVersionBytes, MainNet) case LnTagPrefix.RoutingInfo => RoutingInfo.fromU5s(payload) + + case LnTagPrefix.Features => + LnTag.FeaturesTag(bytes) } tag } + + case class FeaturesTag(features: ByteVector) extends LnTag { + override def prefix: LnTagPrefix = LnTagPrefix.Features + + /** The payload for the tag without any meta information encoded with it */ + override def encoded: Vector[UInt5] = { + Bech32.from8bitTo5bit(features) + } + } } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/ln/PaymentSecret.scala b/core/src/main/scala/org/bitcoins/core/protocol/ln/PaymentSecret.scala new file mode 100644 index 0000000000..824072461e --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/protocol/ln/PaymentSecret.scala @@ -0,0 +1,23 @@ +package org.bitcoins.core.protocol.ln + +import org.bitcoins.core.crypto.{ECPrivateKey, Sha256Digest} +import org.bitcoins.core.protocol.NetworkElement +import org.bitcoins.core.util.{CryptoUtil, Factory} +import scodec.bits.ByteVector + +final case class PaymentSecret(bytes: ByteVector) extends NetworkElement { + require(bytes.size == 32, + s"Payment secret must be 32 bytes in size, got: " + bytes.length) + + lazy val hash: Sha256Digest = CryptoUtil.sha256(bytes) +} + +object PaymentSecret extends Factory[PaymentSecret] { + + override def fromBytes(bytes: ByteVector): PaymentSecret = { + new PaymentSecret(bytes) + } + + def random: PaymentSecret = fromBytes(ECPrivateKey.freshPrivateKey.bytes) + +} 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 93dc6bd96f..884237d24c 100644 --- a/core/src/main/scala/org/bitcoins/core/util/Bech32.scala +++ b/core/src/main/scala/org/bitcoins/core/util/Bech32.scala @@ -302,7 +302,7 @@ sealed abstract class Bech32 { .map(_.toLower) .map { char => val index = Bech32.charset.indexOf(char) - require(index > 0, + require(index >= 0, s"$char (${char.toInt}) is not part of the Bech32 charset!") UInt5(index) }