mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-03 02:39:18 +01:00
Support for payment secret and features LN invoice tags (#1012)
This commit is contained in:
parent
5ee3960e3a
commit
b5d21a5a54
6 changed files with 146 additions and 16 deletions
|
@ -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"))))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue