Support for payment secret and features LN invoice tags (#1012)

This commit is contained in:
rorp 2020-01-07 10:27:45 -08:00 committed by GitHub
parent 5ee3960e3a
commit b5d21a5a54
6 changed files with 146 additions and 16 deletions

View file

@ -1,7 +1,6 @@
package org.bitcoins.core.protocol.ln package org.bitcoins.core.protocol.ln
import org.bitcoins.core.crypto._ 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.number.{UInt32, UInt64, UInt8}
import org.bitcoins.core.protocol.ln.LnParams.{ import org.bitcoins.core.protocol.ln.LnParams.{
LnBitcoinMainNet, 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.ln.routing.LnRoute
import org.bitcoins.core.protocol.{Bech32Address, P2PKHAddress, P2SHAddress} import org.bitcoins.core.protocol.{Bech32Address, P2PKHAddress, P2SHAddress}
import org.bitcoins.core.util.CryptoUtil import org.bitcoins.core.util.CryptoUtil
import org.bitcoins.testkit.core.gen.ln.LnInvoiceGen
import org.bitcoins.testkit.util.BitcoinSUnitTest import org.bitcoins.testkit.util.BitcoinSUnitTest
import scodec.bits.ByteVector import scodec.bits.ByteVector
@ -113,13 +113,17 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
val descriptionTagE = Left(LnTag.DescriptionTag("ナンセンス 1杯")) val descriptionTagE = Left(LnTag.DescriptionTag("ナンセンス 1杯"))
val expiryTag = LnTag.ExpiryTimeTag(UInt32(60)) val expiryTag = LnTag.ExpiryTimeTag(UInt32(60))
val lnTags = LnTaggedFields(paymentTag, val lnTags = LnTaggedFields(
descriptionTagE, paymentHash = paymentTag,
None, secret = None,
Some(expiryTag), descriptionOrHash = descriptionTagE,
None, nodeId = None,
None, expiryTime = Some(expiryTag),
None) cltvExpiry = None,
fallbackAddress = None,
routingInfo = None,
features = None
)
val signature = ECDigitalSignature.fromRS( val signature = ECDigitalSignature.fromRS(
"259f04511e7ef2aa77f6ff04d51b4ae9209504843e5ab9672ce32a153681f687515b73ce57ee309db588a10eb8e41b5a2d2bc17144ddf398033faa49ffe95ae6") "259f04511e7ef2aa77f6ff04d51b4ae9209504843e5ab9672ce32a153681f687515b73ce57ee309db588a10eb8e41b5a2d2bc17144ddf398033faa49ffe95ae6")
@ -359,6 +363,45 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
deserialized.get must be(lnInvoice) 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 { it must "deserialize and reserialize a invoice with a explicity expiry time" in {
//from eclair //from eclair
val bech32 = val bech32 =
@ -449,4 +492,16 @@ class LnInvoiceUnitTest extends BitcoinSUnitTest {
invoice.nodeId.hex must be(expected) 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"))))
}
} }

View file

@ -18,6 +18,9 @@ object LnTagPrefix {
case object PaymentHash extends LnTagPrefix { case object PaymentHash extends LnTagPrefix {
override val value: Char = 'p' override val value: Char = 'p'
} }
case object Secret extends LnTagPrefix {
override val value: Char = 's'
}
case object Description extends LnTagPrefix { case object Description extends LnTagPrefix {
override val value: Char = 'd' override val value: Char = 'd'
} }
@ -47,15 +50,21 @@ object LnTagPrefix {
override val value: Char = 'r' override val value: Char = 'r'
} }
case object Features extends LnTagPrefix {
override val value: Char = '9'
}
private lazy val all: Map[Char, LnTagPrefix] = private lazy val all: Map[Char, LnTagPrefix] =
List(PaymentHash, List(PaymentHash,
Secret,
Description, Description,
NodeId, NodeId,
DescriptionHash, DescriptionHash,
ExpiryTime, ExpiryTime,
CltvExpiry, CltvExpiry,
FallbackAddress, FallbackAddress,
RoutingInfo) RoutingInfo,
Features)
.map(prefix => prefix.value -> prefix) .map(prefix => prefix.value -> prefix)
.toMap .toMap

View file

@ -23,6 +23,8 @@ sealed abstract class LnTaggedFields extends NetworkElement {
def paymentHash: LnTag.PaymentHashTag def paymentHash: LnTag.PaymentHashTag
def secret: Option[LnTag.SecretTag]
def description: Option[LnTag.DescriptionTag] def description: Option[LnTag.DescriptionTag]
def nodeId: Option[LnTag.NodeIdTag] def nodeId: Option[LnTag.NodeIdTag]
@ -37,14 +39,18 @@ sealed abstract class LnTaggedFields extends NetworkElement {
def routingInfo: Option[LnTag.RoutingInfo] def routingInfo: Option[LnTag.RoutingInfo]
def features: Option[LnTag.FeaturesTag]
lazy val data: Vector[UInt5] = Vector(Some(paymentHash), lazy val data: Vector[UInt5] = Vector(Some(paymentHash),
description, description,
nodeId, nodeId,
descriptionHash, descriptionHash,
secret,
expiryTime, expiryTime,
cltvExpiry, cltvExpiry,
fallbackAddress, fallbackAddress,
routingInfo) routingInfo,
features)
.filter(_.isDefined) .filter(_.isDefined)
.flatMap(_.get.data) .flatMap(_.get.data)
@ -70,9 +76,11 @@ object LnTaggedFields {
nodeId: Option[LnTag.NodeIdTag], nodeId: Option[LnTag.NodeIdTag],
descriptionHash: Option[LnTag.DescriptionHashTag], descriptionHash: Option[LnTag.DescriptionHashTag],
expiryTime: Option[LnTag.ExpiryTimeTag], expiryTime: Option[LnTag.ExpiryTimeTag],
secret: Option[LnTag.SecretTag],
cltvExpiry: Option[LnTag.MinFinalCltvExpiry], cltvExpiry: Option[LnTag.MinFinalCltvExpiry],
fallbackAddress: Option[LnTag.FallbackAddressTag], fallbackAddress: Option[LnTag.FallbackAddressTag],
routingInfo: Option[LnTag.RoutingInfo]) routingInfo: Option[LnTag.RoutingInfo],
features: Option[LnTag.FeaturesTag])
extends LnTaggedFields extends LnTaggedFields
/** /**
@ -82,11 +90,13 @@ object LnTaggedFields {
def apply( def apply(
paymentHash: LnTag.PaymentHashTag, paymentHash: LnTag.PaymentHashTag,
descriptionOrHash: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag], descriptionOrHash: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag],
secret: Option[LnTag.SecretTag] = None,
nodeId: Option[LnTag.NodeIdTag] = None, nodeId: Option[LnTag.NodeIdTag] = None,
expiryTime: Option[LnTag.ExpiryTimeTag] = None, expiryTime: Option[LnTag.ExpiryTimeTag] = None,
cltvExpiry: Option[LnTag.MinFinalCltvExpiry] = None, cltvExpiry: Option[LnTag.MinFinalCltvExpiry] = None,
fallbackAddress: Option[LnTag.FallbackAddressTag] = 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): ( val (description, descriptionHash): (
Option[LnTag.DescriptionTag], Option[LnTag.DescriptionTag],
@ -102,13 +112,15 @@ object LnTaggedFields {
InvoiceTagImpl( InvoiceTagImpl(
paymentHash = paymentHash, paymentHash = paymentHash,
secret = secret,
description = description, description = description,
nodeId = nodeId, nodeId = nodeId,
descriptionHash = descriptionHash, descriptionHash = descriptionHash,
expiryTime = expiryTime, expiryTime = expiryTime,
cltvExpiry = cltvExpiry, cltvExpiry = cltvExpiry,
fallbackAddress = fallbackAddress, fallbackAddress = fallbackAddress,
routingInfo = routingInfo routingInfo = routingInfo,
features = features
) )
} }
@ -164,6 +176,8 @@ object LnTaggedFields {
s"Payment hash must be defined in a LnInvoice") s"Payment hash must be defined in a LnInvoice")
) )
val secret = getTag[LnTag.SecretTag]
val description = getTag[LnTag.DescriptionTag] val description = getTag[LnTag.DescriptionTag]
val descriptionHash = getTag[LnTag.DescriptionHashTag] val descriptionHash = getTag[LnTag.DescriptionHashTag]
@ -178,6 +192,8 @@ object LnTaggedFields {
val routingInfo = getTag[LnTag.RoutingInfo] val routingInfo = getTag[LnTag.RoutingInfo]
val features = getTag[LnTag.FeaturesTag]
val d: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag] = { val d: Either[LnTag.DescriptionTag, LnTag.DescriptionHashTag] = {
if (description.isDefined && descriptionHash.isDefined) { if (description.isDefined && descriptionHash.isDefined) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
@ -194,12 +210,14 @@ object LnTaggedFields {
LnTaggedFields( LnTaggedFields(
paymentHash = paymentHashTag, paymentHash = paymentHashTag,
secret = secret,
descriptionOrHash = d, descriptionOrHash = d,
nodeId = nodeId, nodeId = nodeId,
expiryTime = expiryTime, expiryTime = expiryTime,
cltvExpiry = cltvExpiry, cltvExpiry = cltvExpiry,
fallbackAddress = fallbackAddress, fallbackAddress = fallbackAddress,
routingInfo = routingInfo routingInfo = routingInfo,
features = features
) )
} }

View file

@ -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 { case class DescriptionTag(string: String) extends LnTag {
override val prefix: LnTagPrefix = LnTagPrefix.Description override val prefix: LnTagPrefix = LnTagPrefix.Description
@ -261,6 +270,9 @@ object LnTag {
val hash = Sha256Digest.fromBytes(bytes) val hash = Sha256Digest.fromBytes(bytes)
LnTag.PaymentHashTag(hash) LnTag.PaymentHashTag(hash)
case LnTagPrefix.Secret =>
LnTag.SecretTag(PaymentSecret.fromBytes(bytes))
case LnTagPrefix.Description => case LnTagPrefix.Description =>
val description = new String(bytes.toArray, Charset.forName("UTF-8")) val description = new String(bytes.toArray, Charset.forName("UTF-8"))
LnTag.DescriptionTag(description) LnTag.DescriptionTag(description)
@ -287,13 +299,26 @@ object LnTag {
case LnTagPrefix.FallbackAddress => case LnTagPrefix.FallbackAddress =>
val version = payload.head.toUInt8 val version = payload.head.toUInt8
val noVersion = payload.tail val noVersion = payload.tail
val noVersionBytes = UInt8.toBytes(Bech32.from5bitTo8bit(noVersion)) val noVersionBytes =
UInt8.toBytes(Bech32.from5bitTo8bit(noVersion))
FallbackAddressV.fromU8(version, noVersionBytes, MainNet) FallbackAddressV.fromU8(version, noVersionBytes, MainNet)
case LnTagPrefix.RoutingInfo => case LnTagPrefix.RoutingInfo =>
RoutingInfo.fromU5s(payload) RoutingInfo.fromU5s(payload)
case LnTagPrefix.Features =>
LnTag.FeaturesTag(bytes)
} }
tag 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)
}
}
} }

View file

@ -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)
}

View file

@ -302,7 +302,7 @@ sealed abstract class Bech32 {
.map(_.toLower) .map(_.toLower)
.map { char => .map { char =>
val index = Bech32.charset.indexOf(char) val index = Bech32.charset.indexOf(char)
require(index > 0, require(index >= 0,
s"$char (${char.toInt}) is not part of the Bech32 charset!") s"$char (${char.toInt}) is not part of the Bech32 charset!")
UInt5(index) UInt5(index)
} }