1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-19 09:54:02 +01:00

Add Bolt12 types and codecs (#2145)

Add types for representing offers and Bolt12 invoices (lightning/bolts#798)
This commit is contained in:
Thomas HUET 2022-04-13 11:26:03 +02:00 committed by GitHub
parent 787c51acc2
commit c7c515a0ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1532 additions and 71 deletions

View File

@ -62,6 +62,8 @@ case class UnknownFeature(bitIndex: Int)
case class Features[T <: Feature](activated: Map[T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) { case class Features[T <: Feature](activated: Map[T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) {
def isEmpty: Boolean = activated.isEmpty && unknown.isEmpty
def hasFeature(feature: T, support: Option[FeatureSupport] = None): Boolean = support match { def hasFeature(feature: T, support: Option[FeatureSupport] = None): Boolean = support match {
case Some(s) => activated.get(feature).contains(s) case Some(s) => activated.get(feature).contains(s)
case None => activated.contains(feature) case None => activated.contains(feature)

View File

@ -23,7 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute}
import fr.acinq.eclair.db._ import fr.acinq.eclair.db._
import fr.acinq.eclair.db.pg.PgUtils.PgLock import fr.acinq.eclair.db.pg.PgUtils.PgLock
import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentFailed, Invoice, PaymentSent} import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed, PaymentSent}
import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import scodec.bits.BitVector import scodec.bits.BitVector
@ -32,7 +32,6 @@ import java.sql.{Connection, ResultSet, Statement, Timestamp}
import java.time.Instant import java.time.Instant
import java.util.UUID import java.util.UUID
import javax.sql.DataSource import javax.sql.DataSource
import scala.concurrent.duration.DurationLong
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
object PgPaymentsDb { object PgPaymentsDb {
@ -157,7 +156,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit
MilliSatoshi(rs.getLong("recipient_amount_msat")), MilliSatoshi(rs.getLong("recipient_amount_msat")),
PublicKey(rs.getByteVectorFromHex("recipient_node_id")), PublicKey(rs.getByteVectorFromHex("recipient_node_id")),
TimestampMilli(rs.getTimestamp("created_at").getTime), TimestampMilli(rs.getTimestamp("created_at").getTime),
rs.getStringNullable("payment_request").map(Invoice.fromString), rs.getStringNullable("payment_request").map(Invoice.fromString(_).get),
status status
) )
} }
@ -249,7 +248,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit
private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { private def parseIncomingPayment(rs: ResultSet): IncomingPayment = {
val invoice = rs.getString("payment_request") val invoice = rs.getString("payment_request")
IncomingPayment( IncomingPayment(
Bolt11Invoice.fromString(invoice), Bolt11Invoice.fromString(invoice).get,
rs.getByteVector32FromHex("payment_preimage"), rs.getByteVector32FromHex("payment_preimage"),
rs.getString("payment_type"), rs.getString("payment_type"),
TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")), TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")),

View File

@ -23,7 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute}
import fr.acinq.eclair.db._ import fr.acinq.eclair.db._
import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.eclair.db.sqlite.SqliteUtils._
import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentFailed, Invoice, PaymentSent} import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed, PaymentSent}
import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import scodec.bits.BitVector import scodec.bits.BitVector
@ -178,7 +178,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging {
MilliSatoshi(rs.getLong("recipient_amount_msat")), MilliSatoshi(rs.getLong("recipient_amount_msat")),
PublicKey(rs.getByteVector("recipient_node_id")), PublicKey(rs.getByteVector("recipient_node_id")),
TimestampMilli(rs.getLong("created_at")), TimestampMilli(rs.getLong("created_at")),
rs.getStringNullable("payment_request").map(Invoice.fromString), rs.getStringNullable("payment_request").map(Invoice.fromString(_).get),
status status
) )
} }
@ -256,7 +256,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging {
private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { private def parseIncomingPayment(rs: ResultSet): IncomingPayment = {
val invoice = rs.getString("payment_request") val invoice = rs.getString("payment_request")
IncomingPayment( IncomingPayment(
Bolt11Invoice.fromString(invoice), Bolt11Invoice.fromString(invoice).get,
rs.getByteVector32("payment_preimage"), rs.getByteVector32("payment_preimage"),
rs.getString("payment_type"), rs.getString("payment_type"),
TimestampMilli(rs.getLong("created_at")), TimestampMilli(rs.getLong("created_at")),

View File

@ -30,7 +30,7 @@ import scala.util.{Failure, Success, Try}
/** /**
* Lightning Bolt 11 invoice * Lightning Bolt 11 invoice
* see https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md * see https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
* *
* @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet * @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet
* @param amount_opt amount to pay (empty string means no amount is specified) * @param amount_opt amount to pay (empty string means no amount is specified)
@ -46,8 +46,11 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat
amount_opt.foreach(a => require(a > 0.msat, s"amount is not valid")) amount_opt.foreach(a => require(a > 0.msat, s"amount is not valid"))
require(tags.collect { case _: Bolt11Invoice.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") require(tags.collect { case _: Bolt11Invoice.PaymentHash => }.size == 1, "there must be exactly one payment hash tag")
require(tags.collect { case Bolt11Invoice.Description(_) | Bolt11Invoice.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag") require(tags.collect { case Bolt11Invoice.Description(_) | Bolt11Invoice.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag")
private val featuresErr = Features.validateFeatureGraph(features)
{
val featuresErr = Features.validateFeatureGraph(features)
require(featuresErr.isEmpty, featuresErr.map(_.message)) require(featuresErr.isEmpty, featuresErr.map(_.message))
}
if (features.hasFeature(Features.PaymentSecret)) { if (features.hasFeature(Features.PaymentSecret)) {
require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set") require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set")
} }
@ -508,13 +511,13 @@ object Bolt11Invoice {
// TODO: could be optimized by preallocating the resulting buffer // TODO: could be optimized by preallocating the resulting buffer
def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _) def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _)
val eight2fiveCodec: Codec[List[Byte]] = list(ubyte(5)) val eight2fiveCodec: Codec[List[java.lang.Byte]] = list(ubyte(5).xmap[java.lang.Byte](b => b, b => b))
/** /**
* @param input bech32-encoded invoice * @param input bech32-encoded invoice
* @return a Bolt11 invoice * @return a Bolt11 invoice
*/ */
def fromString(input: String): Bolt11Invoice = { def fromString(input: String): Try[Bolt11Invoice] = Try {
// used only for data validation // used only for data validation
Bech32.decode(input, false) Bech32.decode(input, false)
val lowercaseInput = input.toLowerCase val lowercaseInput = input.toLowerCase

View File

@ -0,0 +1,183 @@
/*
* Copyright 2022 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.payment
import fr.acinq.bitcoin.Bech32
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding
import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop
import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceTlvCodec}
import fr.acinq.eclair.wire.protocol.Offers._
import fr.acinq.eclair.wire.protocol.{Offers, TlvStream}
import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond}
import scodec.bits.ByteVector
import java.util.concurrent.TimeUnit
import scala.concurrent.duration.FiniteDuration
import scala.util.Try
/**
* Lightning Bolt 12 invoice
* see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
*/
case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[PublicKey]) extends Invoice {
import Bolt12Invoice._
require(records.get[Amount].nonEmpty, "bolt 12 invoices must provide an amount")
require(records.get[NodeId].nonEmpty, "bolt 12 invoices must provide a node id")
require(records.get[PaymentHash].nonEmpty, "bolt 12 invoices must provide a payment hash")
require(records.get[Description].nonEmpty, "bolt 12 invoices must provide a description")
require(records.get[CreatedAt].nonEmpty, "bolt 12 invoices must provide a creation timestamp")
require(records.get[Signature].nonEmpty, "bolt 12 invoices must provide a signature")
val amount: MilliSatoshi = records.get[Amount].map(_.amount).get
override val amount_opt: Option[MilliSatoshi] = Some(amount)
override val nodeId: Crypto.PublicKey = nodeId_opt.getOrElse(records.get[NodeId].get.nodeId1)
override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash
override val paymentSecret: Option[ByteVector32] = None
override val paymentMetadata: Option[ByteVector] = None
override val description: Either[String, ByteVector32] = Left(records.get[Description].get.description)
override val routingInfo: Seq[Seq[ExtraHop]] = Seq.empty
override val createdAt: TimestampSecond = records.get[CreatedAt].get.timestamp
override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[RelativeExpiry].map(_.seconds).getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS)
override val minFinalCltvExpiryDelta: CltvExpiryDelta = records.get[Cltv].map(_.minFinalCltvExpiry).getOrElse(DEFAULT_MIN_FINAL_EXPIRY_DELTA)
override val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty)
override def toString: String = {
val data = invoiceCodec.encode(this).require.bytes
Bech32.encodeBytes(hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum)
}
val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash)
val offerId: Option[ByteVector32] = records.get[OfferId].map(_.offerId)
val blindedPaths: Option[Seq[RouteBlinding.BlindedRoute]] = records.get[Paths].map(_.paths)
val issuer: Option[String] = records.get[Issuer].map(_.issuer)
val quantity: Option[Long] = records.get[Quantity].map(_.quantity)
val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash)
val payerKey: Option[ByteVector32] = records.get[PayerKey].map(_.publicKey)
val payerNote: Option[String] = records.get[PayerNote].map(_.note)
val payerInfo: Option[ByteVector] = records.get[PayerInfo].map(_.info)
val fallbacks: Option[Seq[FallbackAddress]] = records.get[Fallbacks].map(_.addresses)
val refundSignature: Option[ByteVector64] = records.get[RefundSignature].map(_.signature)
val replaceInvoice: Option[ByteVector32] = records.get[ReplaceInvoice].map(_.paymentHash)
val signature: ByteVector64 = records.get[Signature].get.signature
// It is assumed that the request is valid for this offer.
def isValidFor(offer: Offer, request: InvoiceRequest): Boolean = {
Offers.xOnlyPublicKey(nodeId) == offer.nodeIdXOnly &&
checkSignature() &&
offerId.contains(request.offerId) &&
request.chain == chain &&
!isExpired() &&
request.amount.contains(amount) &&
quantity == request.quantity_opt &&
payerKey.contains(request.payerKey) &&
payerInfo == request.payerInfo &&
// Bolt 12: MUST reject the invoice if payer_note is set, and was unset or not equal to the field in the invoice_request.
payerNote.forall(request.payerNote.contains(_)) &&
description.swap.exists(_.startsWith(offer.description)) &&
issuer == offer.issuer &&
request.features.areSupported(features)
}
def checkRefundSignature(): Boolean = {
(refundSignature, refundFor, payerKey) match {
case (Some(sig), Some(hash), Some(key)) => verifySchnorr(signatureTag("payer_signature"), hash, sig, key)
case _ => false
}
}
def checkSignature(): Boolean = {
verifySchnorr(signatureTag("signature"), rootHash(Offers.removeSignature(records), invoiceTlvCodec), signature, Offers.xOnlyPublicKey(nodeId))
}
def withNodeId(nodeId: PublicKey): Bolt12Invoice = Bolt12Invoice(records, Some(nodeId))
}
object Bolt12Invoice {
val hrp = "lni"
val DEFAULT_EXPIRY_SECONDS: Long = 7200
val DEFAULT_MIN_FINAL_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(18)
/**
* Creates an invoice for a given offer and invoice request.
*
* @param offer the offer this invoice corresponds to
* @param request the request this invoice responds to
* @param preimage the preimage to use for the payment
* @param nodeKey the key that was used to generate the offer, may be different from our public nodeId if we're hiding behind a blinded route
* @param features invoice features
*/
def apply(offer: Offer, request: InvoiceRequest, preimage: ByteVector32, nodeKey: PrivateKey, features: Features[InvoiceFeature]): Bolt12Invoice = {
require(request.amount.nonEmpty || offer.amount.nonEmpty)
val tlvs: Seq[InvoiceTlv] = Seq(
Some(Chain(request.chain)),
Some(CreatedAt(TimestampSecond.now())),
Some(PaymentHash(Crypto.sha256(preimage))),
Some(OfferId(offer.offerId)),
Some(NodeId(nodeKey.publicKey)),
Some(Amount(request.amount.orElse(offer.amount.map(_ * request.quantity)).get)),
Some(Description(offer.description)),
request.quantity_opt.map(Quantity),
Some(PayerKey(request.payerKey)),
request.payerInfo.map(PayerInfo),
request.payerNote.map(PayerNote),
request.replaceInvoice.map(ReplaceInvoice),
offer.issuer.map(Issuer),
Some(FeaturesTlv(features.unscoped()))
).flatten
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), Some(nodeKey.publicKey))
}
def signatureTag(fieldName: String): String = "lightning" + "invoice" + fieldName
def fromString(input: String): Try[Bolt12Invoice] = Try {
val triple = Bech32.decodeBytes(input.toLowerCase, true)
val prefix = triple.getFirst
val encoded = triple.getSecond
val encoding = triple.getThird
require(prefix == hrp)
require(encoding == Bech32.Encoding.Beck32WithoutChecksum)
invoiceCodec.decode(ByteVector(encoded).bits).require.value
}
}

View File

@ -22,6 +22,7 @@ import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi,
import scodec.bits.ByteVector import scodec.bits.ByteVector
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
import scala.util.Try
trait Invoice { trait Invoice {
val amount_opt: Option[MilliSatoshi] val amount_opt: Option[MilliSatoshi]
@ -52,7 +53,11 @@ trait Invoice {
} }
object Invoice { object Invoice {
def fromString(input: String): Invoice = { def fromString(input: String): Try[Invoice] = {
if (input.toLowerCase.startsWith("lni")) {
Bolt12Invoice.fromString(input)
} else {
Bolt11Invoice.fromString(input) Bolt11Invoice.fromString(input)
} }
} }
}

View File

@ -72,7 +72,7 @@ object Scripts {
def encodeNumber(n: Long): ScriptElt = n match { def encodeNumber(n: Long): ScriptElt = n match {
case 0 => OP_0 case 0 => OP_0
case -1 => OP_1NEGATE case -1 => OP_1NEGATE
case x if x >= 1 && x <= 16 => ScriptElt.code2elt((ScriptElt.elt2code(OP_1) + x - 1).toInt) case x if x >= 1 && x <= 16 => ScriptElt.code2elt((ScriptElt.elt2code(OP_1) + x - 1).toInt).get
case _ => OP_PUSHDATA(Script.encodeNumber(n)) case _ => OP_PUSHDATA(Script.encodeNumber(n))
} }

View File

@ -19,6 +19,8 @@ package fr.acinq.eclair.wire.protocol
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.UInt64 import fr.acinq.eclair.UInt64
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute}
import fr.acinq.eclair.payment.Bolt12Invoice
import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceErrorCodec, invoiceRequestCodec}
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv} import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv}
import scodec.bits.ByteVector import scodec.bits.ByteVector
@ -40,6 +42,24 @@ object OnionMessagePayloadTlv {
*/ */
case class EncryptedData(data: ByteVector) extends OnionMessagePayloadTlv case class EncryptedData(data: ByteVector) extends OnionMessagePayloadTlv
/**
* In order to pay a Bolt 12 offer, we must send an onion message to request an invoice corresponding to that offer.
* The creator of the offer will send us an invoice back through our blinded reply path.
*/
case class InvoiceRequest(request: Offers.InvoiceRequest) extends OnionMessagePayloadTlv
/**
* When receiving an invoice request, we must send an onion message back containing an invoice corresponding to the
* requested offer (if it was an offer we published).
*/
case class Invoice(invoice: Bolt12Invoice) extends OnionMessagePayloadTlv
/**
* This message may be used when we receive an invalid invoice or invoice request.
* It contains information helping senders figure out why their message was invalid.
*/
case class InvoiceError(error: Offers.InvoiceError) extends OnionMessagePayloadTlv
} }
object MessageOnion { object MessageOnion {
@ -62,6 +82,9 @@ object MessageOnion {
case class FinalPayload(records: TlvStream[OnionMessagePayloadTlv]) extends PerHopPayload { case class FinalPayload(records: TlvStream[OnionMessagePayloadTlv]) extends PerHopPayload {
val replyPath: Option[OnionMessagePayloadTlv.ReplyPath] = records.get[OnionMessagePayloadTlv.ReplyPath] val replyPath: Option[OnionMessagePayloadTlv.ReplyPath] = records.get[OnionMessagePayloadTlv.ReplyPath]
val encryptedData: ByteVector = records.get[OnionMessagePayloadTlv.EncryptedData].get.data val encryptedData: ByteVector = records.get[OnionMessagePayloadTlv.EncryptedData].get.data
val invoiceRequest: Option[OnionMessagePayloadTlv.InvoiceRequest] = records.get[OnionMessagePayloadTlv.InvoiceRequest]
val invoice: Option[OnionMessagePayloadTlv.Invoice] = records.get[OnionMessagePayloadTlv.Invoice]
val invoiceError: Option[OnionMessagePayloadTlv.InvoiceError] = records.get[OnionMessagePayloadTlv.InvoiceError]
} }
/** Content of the encrypted data of a final node's per-hop payload. */ /** Content of the encrypted data of a final node's per-hop payload. */
@ -90,6 +113,10 @@ object MessageOnionCodecs {
private val onionTlvCodec = discriminated[OnionMessagePayloadTlv].by(varint) private val onionTlvCodec = discriminated[OnionMessagePayloadTlv].by(varint)
.typecase(UInt64(2), replyPathCodec) .typecase(UInt64(2), replyPathCodec)
.typecase(UInt64(4), encryptedDataCodec) .typecase(UInt64(4), encryptedDataCodec)
.typecase(UInt64(64), variableSizeBytesLong(varintoverflow, invoiceRequestCodec.as[InvoiceRequest]))
.typecase(UInt64(66), variableSizeBytesLong(varintoverflow, invoiceCodec.as[Invoice]))
.typecase(UInt64(68), variableSizeBytesLong(varintoverflow, invoiceErrorCodec.as[InvoiceError]))
val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete

View File

@ -0,0 +1,222 @@
/*
* Copyright 2022 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.wire.protocol
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute}
import fr.acinq.eclair.payment.Bolt12Invoice
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.Offers._
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tmillisatoshi, tu32, tu64overflow}
import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, TimestampSecond, UInt64}
import scodec.codecs._
import scodec.{Attempt, Codec}
object OfferCodecs {
private val chains: Codec[Chains] = variableSizeBytesLong(varintoverflow, list(bytes32)).xmap[Seq[ByteVector32]](_.toSeq, _.toList).as[Chains]
private val currency: Codec[Currency] = variableSizeBytesLong(varintoverflow, utf8).as[Currency]
private val amount: Codec[Amount] = variableSizeBytesLong(varintoverflow, tmillisatoshi).as[Amount]
private val description: Codec[Description] = variableSizeBytesLong(varintoverflow, utf8).as[Description]
private val features: Codec[FeaturesTlv] = variableSizeBytesLong(varintoverflow, bytes).xmap[Features[Feature]](Features(_), _.toByteVector).as[FeaturesTlv]
private val absoluteExpiry: Codec[AbsoluteExpiry] = variableSizeBytesLong(varintoverflow, tu64overflow).as[TimestampSecond].as[AbsoluteExpiry]
private val blindedNodeCodec: Codec[BlindedNode] = (("nodeId" | publicKey) :: ("encryptedData" | variableSizeBytes(uint16, bytes))).as[BlindedNode]
private val pathCodec: Codec[BlindedRoute] = (("firstNodeId" | publicKey) :: ("blinding" | publicKey) :: ("path" | listOfN(uint8, blindedNodeCodec).xmap[Seq[BlindedNode]](_.toSeq, _.toList))).as[BlindedRoute]
private val paths: Codec[Paths] = variableSizeBytesLong(varintoverflow, list(pathCodec)).xmap[Seq[BlindedRoute]](_.toSeq, _.toList).as[Paths]
private val issuer: Codec[Issuer] = variableSizeBytesLong(varintoverflow, utf8).as[Issuer]
private val quantityMin: Codec[QuantityMin] = variableSizeBytesLong(varintoverflow, tu64overflow).as[QuantityMin]
private val quantityMax: Codec[QuantityMax] = variableSizeBytesLong(varintoverflow, tu64overflow).as[QuantityMax]
private val nodeId: Codec[NodeId] = variableSizeBytesLong(varintoverflow, bytes32).as[NodeId]
private val sendInvoice: Codec[SendInvoice] = variableSizeBytesLong(varintoverflow, provide(SendInvoice()))
private val refundFor: Codec[RefundFor] = variableSizeBytesLong(varintoverflow, bytes32).as[RefundFor]
private val signature: Codec[Signature] = variableSizeBytesLong(varintoverflow, bytes64).as[Signature]
val offerTlvCodec: Codec[TlvStream[OfferTlv]] = TlvCodecs.tlvStream[OfferTlv](discriminated[OfferTlv].by(varint)
.typecase(UInt64(2), chains)
.typecase(UInt64(6), currency)
.typecase(UInt64(8), amount)
.typecase(UInt64(10), description)
.typecase(UInt64(12), features)
.typecase(UInt64(14), absoluteExpiry)
.typecase(UInt64(16), paths)
.typecase(UInt64(20), issuer)
.typecase(UInt64(22), quantityMin)
.typecase(UInt64(24), quantityMax)
.typecase(UInt64(30), nodeId)
.typecase(UInt64(34), refundFor)
.typecase(UInt64(54), sendInvoice)
.typecase(UInt64(240), signature)).complete
val offerCodec: Codec[Offer] = offerTlvCodec.narrow({ tlvs =>
if (tlvs.get[Description].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(10)))
}
if (tlvs.get[NodeId].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(30)))
}
Attempt.successful(Offer(tlvs))
}, {
case Offer(tlvs) => tlvs
})
private val chain: Codec[Chain] = variableSizeBytesLong(varintoverflow, bytes32).as[Chain]
private val offerId: Codec[OfferId] = variableSizeBytesLong(varintoverflow, bytes32).as[OfferId]
private val quantity: Codec[Quantity] = variableSizeBytesLong(varintoverflow, tu64overflow).as[Quantity]
private val payerKey: Codec[PayerKey] = variableSizeBytesLong(varintoverflow, bytes32).as[PayerKey]
private val payerNote: Codec[PayerNote] = variableSizeBytesLong(varintoverflow, utf8).as[PayerNote]
private val payerInfo: Codec[PayerInfo] = variableSizeBytesLong(varintoverflow, bytes).as[PayerInfo]
private val replaceInvoice: Codec[ReplaceInvoice] = variableSizeBytesLong(varintoverflow, bytes32).as[ReplaceInvoice]
val invoiceRequestTlvCodec: Codec[TlvStream[InvoiceRequestTlv]] = TlvCodecs.tlvStream[InvoiceRequestTlv](discriminated[InvoiceRequestTlv].by(varint)
.typecase(UInt64(3), chain)
.typecase(UInt64(4), offerId)
.typecase(UInt64(8), amount)
.typecase(UInt64(12), features)
.typecase(UInt64(32), quantity)
.typecase(UInt64(38), payerKey)
.typecase(UInt64(39), payerNote)
.typecase(UInt64(50), payerInfo)
.typecase(UInt64(56), replaceInvoice)
.typecase(UInt64(240), signature)).complete
val invoiceRequestCodec: Codec[InvoiceRequest] = invoiceRequestTlvCodec.narrow({ tlvs =>
if (tlvs.get[OfferId].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(4)))
} else if (tlvs.get[PayerKey].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(38)))
} else if (tlvs.get[Signature].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(240)))
} else {
Attempt.successful(InvoiceRequest(tlvs))
}
}, {
case InvoiceRequest(tlvs) => tlvs
})
private val paymentInfo: Codec[PaymentInfo] = (("fee_base_msat" | millisatoshi32) ::
("fee_proportional_millionths" | tu32) ::
("cltv_expiry_delta" | cltvExpiryDelta)).as[PaymentInfo]
private val paymentPathsInfo: Codec[PaymentPathsInfo] = variableSizeBytesLong(varintoverflow, list(paymentInfo)).xmap[Seq[PaymentInfo]](_.toSeq, _.toList).as[PaymentPathsInfo]
private val paymentConstraints: Codec[PaymentConstraints] = (("max_cltv_expiry" | cltvExpiry) ::
("htlc_minimum_msat" | millisatoshi) ::
("allowed_features" | bytes.xmap[Features[Feature]](Features(_), _.toByteVector))).as[PaymentConstraints]
private val paymentPathsConstraints: Codec[PaymentPathsConstraints] = variableSizeBytesLong(varintoverflow, list(paymentConstraints)).xmap[Seq[PaymentConstraints]](_.toSeq, _.toList).as[PaymentPathsConstraints]
private val createdAt: Codec[CreatedAt] = variableSizeBytesLong(varintoverflow, tu64overflow).as[TimestampSecond].as[CreatedAt]
private val paymentHash: Codec[PaymentHash] = variableSizeBytesLong(varintoverflow, bytes32).as[PaymentHash]
private val relativeExpiry: Codec[RelativeExpiry] = variableSizeBytesLong(varintoverflow, tu32).as[RelativeExpiry]
private val cltv: Codec[Cltv] = variableSizeBytesLong(varintoverflow, uint16).as[CltvExpiryDelta].as[Cltv]
private val fallbackAddress: Codec[FallbackAddress] = variableSizeBytesLong(varintoverflow,
("version" | byte) ::
("address" | variableSizeBytes(uint16, bytes))).as[FallbackAddress]
private val fallbacks: Codec[Fallbacks] = variableSizeBytesLong(varintoverflow, list(fallbackAddress)).xmap[Seq[FallbackAddress]](_.toSeq, _.toList).as[Fallbacks]
private val refundSignature: Codec[RefundSignature] = variableSizeBytesLong(varintoverflow, bytes64).as[RefundSignature]
val invoiceTlvCodec: Codec[TlvStream[InvoiceTlv]] = TlvCodecs.tlvStream[InvoiceTlv](discriminated[InvoiceTlv].by(varint)
.typecase(UInt64(3), chain)
.typecase(UInt64(4), offerId)
.typecase(UInt64(8), amount)
.typecase(UInt64(10), description)
.typecase(UInt64(12), features)
// TODO: the spec for payment paths is not final, adjust codecs if changes are made to he spec.
.typecase(UInt64(16), paths)
.typecase(UInt64(18), paymentPathsInfo)
.typecase(UInt64(19), paymentPathsConstraints)
.typecase(UInt64(20), issuer)
.typecase(UInt64(30), nodeId)
.typecase(UInt64(32), quantity)
.typecase(UInt64(34), refundFor)
.typecase(UInt64(38), payerKey)
.typecase(UInt64(39), payerNote)
.typecase(UInt64(40), createdAt)
.typecase(UInt64(42), paymentHash)
.typecase(UInt64(44), relativeExpiry)
.typecase(UInt64(46), cltv)
.typecase(UInt64(48), fallbacks)
.typecase(UInt64(50), payerInfo)
.typecase(UInt64(52), refundSignature)
.typecase(UInt64(56), replaceInvoice)
.typecase(UInt64(240), signature)
).complete
val invoiceCodec: Codec[Bolt12Invoice] = invoiceTlvCodec.narrow({ tlvs =>
if (tlvs.get[Amount].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(8)))
} else if (tlvs.get[Description].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(10)))
} else if (tlvs.get[NodeId].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(30)))
} else if (tlvs.get[CreatedAt].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(40)))
} else if (tlvs.get[PaymentHash].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(42)))
} else if (tlvs.get[Signature].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(240)))
} else {
Attempt.successful(Bolt12Invoice(tlvs, None))
}
}, {
case Bolt12Invoice(tlvs, _) => tlvs
})
val invoiceErrorTlvCodec: Codec[TlvStream[InvoiceErrorTlv]] = TlvCodecs.tlvStream[InvoiceErrorTlv](discriminated[InvoiceErrorTlv].by(varint)
.typecase(UInt64(1), variableSizeBytesLong(varintoverflow, tu64overflow).as[ErroneousField])
.typecase(UInt64(3), variableSizeBytesLong(varintoverflow, bytes).as[SuggestedValue])
.typecase(UInt64(5), variableSizeBytesLong(varintoverflow, utf8).as[Error])
).complete
val invoiceErrorCodec: Codec[InvoiceError] = invoiceErrorTlvCodec.narrow({ tlvs =>
if (tlvs.get[Error].isEmpty) {
Attempt.failure(MissingRequiredTlv(UInt64(5)))
} else {
Attempt.successful(InvoiceError(tlvs))
}
}, {
case InvoiceError(tlvs) => tlvs
})
}

View File

@ -0,0 +1,387 @@
/*
* Copyright 2022 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.wire.protocol
import fr.acinq.bitcoin.Bech32
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, LexicographicalOrdering}
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute
import fr.acinq.eclair.message.OnionMessages
import fr.acinq.eclair.wire.protocol.OfferCodecs._
import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, InvoiceFeature, MilliSatoshi, TimestampSecond}
import fr.acinq.secp256k1.Secp256k1JvmKt
import scodec.Codec
import scodec.bits.ByteVector
import scodec.codecs.vector
import scala.util.Try
/**
* Lightning Bolt 12 offers
* see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
*/
object Offers {
sealed trait Bolt12Tlv extends Tlv
sealed trait OfferTlv extends Bolt12Tlv
sealed trait InvoiceRequestTlv extends Bolt12Tlv
sealed trait InvoiceTlv extends Bolt12Tlv
sealed trait InvoiceErrorTlv extends Bolt12Tlv
case class Chains(chains: Seq[ByteVector32]) extends OfferTlv
case class Currency(iso4217: String) extends OfferTlv
case class Amount(amount: MilliSatoshi) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv
case class Description(description: String) extends OfferTlv with InvoiceTlv
case class FeaturesTlv(features: Features[Feature]) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv
case class AbsoluteExpiry(absoluteExpiry: TimestampSecond) extends OfferTlv
case class Paths(paths: Seq[BlindedRoute]) extends OfferTlv with InvoiceTlv
case class PaymentInfo(feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta)
case class PaymentPathsInfo(paymentInfo: Seq[PaymentInfo]) extends InvoiceTlv
case class PaymentConstraints(maxCltvExpiry: CltvExpiry, minHtlc: MilliSatoshi, allowedFeatures: Features[Feature])
case class PaymentPathsConstraints(paymentConstraints: Seq[PaymentConstraints]) extends InvoiceTlv
case class Issuer(issuer: String) extends OfferTlv with InvoiceTlv
case class QuantityMin(min: Long) extends OfferTlv
case class QuantityMax(max: Long) extends OfferTlv
case class NodeId(xonly: ByteVector32) extends OfferTlv with InvoiceTlv {
val nodeId1: PublicKey = PublicKey(2 +: xonly)
val nodeId2: PublicKey = PublicKey(3 +: xonly)
}
object NodeId {
def apply(publicKey: PublicKey): NodeId = NodeId(xOnlyPublicKey(publicKey))
}
case class SendInvoice() extends OfferTlv with InvoiceTlv
case class RefundFor(refundedPaymentHash: ByteVector32) extends OfferTlv with InvoiceTlv
case class Signature(signature: ByteVector64) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv
case class Chain(hash: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv
case class OfferId(offerId: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv
case class Quantity(quantity: Long) extends InvoiceRequestTlv with InvoiceTlv
case class PayerKey(publicKey: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv
object PayerKey {
def apply(publicKey: PublicKey): PayerKey = PayerKey(xOnlyPublicKey(publicKey))
}
case class PayerNote(note: String) extends InvoiceRequestTlv with InvoiceTlv
case class PayerInfo(info: ByteVector) extends InvoiceRequestTlv with InvoiceTlv
case class ReplaceInvoice(paymentHash: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv
case class CreatedAt(timestamp: TimestampSecond) extends InvoiceTlv
case class PaymentHash(hash: ByteVector32) extends InvoiceTlv
case class RelativeExpiry(seconds: Long) extends InvoiceTlv
case class Cltv(minFinalCltvExpiry: CltvExpiryDelta) extends InvoiceTlv
case class FallbackAddress(version: Byte, value: ByteVector)
case class Fallbacks(addresses: Seq[FallbackAddress]) extends InvoiceTlv
case class RefundSignature(signature: ByteVector64) extends InvoiceTlv
case class ErroneousField(tag: Long) extends InvoiceErrorTlv
case class SuggestedValue(value: ByteVector) extends InvoiceErrorTlv
case class Error(message: String) extends InvoiceErrorTlv
case class Offer(records: TlvStream[OfferTlv]) {
require(records.get[NodeId].nonEmpty, "bolt 12 offers must provide a node id")
require(records.get[Description].nonEmpty, "bolt 12 offers must provide a description")
val offerId: ByteVector32 = rootHash(removeSignature(records), offerTlvCodec)
val chains: Seq[ByteVector32] = records.get[Chains].map(_.chains).getOrElse(Seq(Block.LivenetGenesisBlock.hash))
val currency: Option[String] = records.get[Currency].map(_.iso4217)
val amount: Option[MilliSatoshi] = currency match {
case Some(_) => None // TODO: add exchange rates
case None => records.get[Amount].map(_.amount)
}
val description: String = records.get[Description].get.description
val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty)
val expiry: Option[TimestampSecond] = records.get[AbsoluteExpiry].map(_.absoluteExpiry)
val issuer: Option[String] = records.get[Issuer].map(_.issuer)
val quantityMin: Option[Long] = records.get[QuantityMin].map(_.min)
val quantityMax: Option[Long] = records.get[QuantityMax].map(_.max)
val nodeIdXOnly: ByteVector32 = records.get[NodeId].get.xonly
val sendInvoice: Boolean = records.get[SendInvoice].nonEmpty
val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash)
val signature: Option[ByteVector64] = records.get[Signature].map(_.signature)
val contact: Seq[OnionMessages.Destination] =
records.get[Paths].flatMap(_.paths.headOption).map(OnionMessages.BlindedPath).map(Seq(_))
.getOrElse(Seq(PublicKey(2.toByte +: nodeIdXOnly), PublicKey(3.toByte +: nodeIdXOnly)).map(nodeId => OnionMessages.Recipient(nodeId, None, None)))
def sign(key: PrivateKey): Offer = {
val sig = signSchnorr(Offer.signatureTag, rootHash(records, offerTlvCodec), key)
Offer(TlvStream[OfferTlv](records.records ++ Seq(Signature(sig)), records.unknown))
}
def checkSignature(): Boolean = {
signature match {
case Some(sig) => verifySchnorr(Offer.signatureTag, rootHash(removeSignature(records), offerTlvCodec), sig, nodeIdXOnly)
case None => false
}
}
def encode(): String = {
val data = offerCodec.encode(this).require.bytes
Bech32.encodeBytes(Offer.hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum)
}
override def toString: String = encode()
}
object Offer {
val hrp = "lno"
val signatureTag: String = "lightning" + "offer" + "signature"
/**
* @param amount_opt amount if it can be determined at offer creation time.
* @param description description of the offer.
* @param nodeId the nodeId to use for this offer, which should be different from our public nodeId if we're hiding behind a blinded route.
* @param features invoice features.
* @param chain chain on which the offer is valid.
*/
def apply(amount_opt: Option[MilliSatoshi], description: String, nodeId: PublicKey, features: Features[InvoiceFeature], chain: ByteVector32): Offer = {
val tlvs: Seq[OfferTlv] = Seq(
if (chain != Block.LivenetGenesisBlock.hash) Some(Chains(Seq(chain))) else None,
amount_opt.map(Amount),
Some(Description(description)),
Some(NodeId(nodeId)),
if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None,
).flatten
Offer(TlvStream(tlvs))
}
def decode(s: String): Try[Offer] = Try {
val triple = Bech32.decodeBytes(s.toLowerCase, true)
val prefix = triple.getFirst
val encoded = triple.getSecond
val encoding = triple.getThird
require(prefix == hrp)
require(encoding == Bech32.Encoding.Beck32WithoutChecksum)
offerCodec.decode(ByteVector(encoded).bits).require.value
}
}
case class InvoiceRequest(records: TlvStream[InvoiceRequestTlv]) {
require(records.get[OfferId].nonEmpty, "bolt 12 invoice requests must provide an offer id")
require(records.get[PayerKey].nonEmpty, "bolt 12 invoice requests must provide a payer key")
require(records.get[Signature].nonEmpty, "bolt 12 invoice requests must provide a payer signature")
val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash)
val offerId: ByteVector32 = records.get[OfferId].map(_.offerId).get
val amount: Option[MilliSatoshi] = records.get[Amount].map(_.amount)
val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty)
val quantity_opt: Option[Long] = records.get[Quantity].map(_.quantity)
val quantity: Long = quantity_opt.getOrElse(1)
val payerKey: ByteVector32 = records.get[PayerKey].get.publicKey
val payerNote: Option[String] = records.get[PayerNote].map(_.note)
val payerInfo: Option[ByteVector] = records.get[PayerInfo].map(_.info)
val replaceInvoice: Option[ByteVector32] = records.get[ReplaceInvoice].map(_.paymentHash)
val payerSignature: ByteVector64 = records.get[Signature].get.signature
def isValidFor(offer: Offer): Boolean = {
val amountOk = offer.amount match {
case Some(offerAmount) =>
val baseInvoiceAmount = offerAmount * quantity
amount.forall(baseInvoiceAmount <= _)
case None => amount.nonEmpty
}
amountOk &&
offer.offerId == offerId &&
offer.chains.contains(chain) &&
offer.quantityMin.forall(min => quantity_opt.nonEmpty && min <= quantity) &&
offer.quantityMax.forall(max => quantity_opt.nonEmpty && quantity <= max) &&
quantity_opt.forall(_ => offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) &&
offer.features.areSupported(features) &&
checkSignature()
}
def checkSignature(): Boolean = {
verifySchnorr(InvoiceRequest.signatureTag, rootHash(removeSignature(records), invoiceRequestTlvCodec), payerSignature, payerKey)
}
def encode(): String = {
val data = invoiceRequestCodec.encode(this).require.bytes
Bech32.encodeBytes(InvoiceRequest.hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum)
}
override def toString: String = encode()
}
object InvoiceRequest {
val hrp = "lnr"
val signatureTag: String = "lightning" + "invoice_request" + "payer_signature"
/**
* Create a request to fetch an invoice for a given offer.
*
* @param offer Bolt 12 offer.
* @param amount amount that we want to pay.
* @param quantity quantity of items we're buying.
* @param features invoice features.
* @param payerKey private key identifying the payer: this lets us prove we're the ones who paid the invoice.
* @param chain chain we want to use to pay this offer.
*/
def apply(offer: Offer, amount: MilliSatoshi, quantity: Long, features: Features[InvoiceFeature], payerKey: PrivateKey, chain: ByteVector32): InvoiceRequest = {
require(offer.chains contains chain)
require(quantity == 1 || offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty)
val tlvs: Seq[InvoiceRequestTlv] = Seq(
Some(Chain(chain)),
Some(OfferId(offer.offerId)),
Some(Amount(amount)),
if (offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) Some(Quantity(quantity)) else None,
Some(PayerKey(payerKey.publicKey)),
Some(FeaturesTlv(features.unscoped()))
).flatten
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey)
InvoiceRequest(TlvStream(tlvs :+ Signature(signature)))
}
def decode(s: String): Try[InvoiceRequest] = Try {
val triple = Bech32.decodeBytes(s.toLowerCase, true)
val prefix = triple.getFirst
val encoded = triple.getSecond
val encoding = triple.getThird
require(prefix == hrp)
require(encoding == Bech32.Encoding.Beck32WithoutChecksum)
invoiceRequestCodec.decode(ByteVector(encoded).bits).require.value
}
}
case class InvoiceError(records: TlvStream[InvoiceErrorTlv]) {
require(records.get[Error].nonEmpty, "bolt 12 invoice errors must provide an explanatory string")
}
def rootHash[T <: Tlv](tlvs: TlvStream[T], codec: Codec[TlvStream[T]]): ByteVector32 = {
// Encoding tlvs is always safe, unless we have a bug in our codecs, so we can call `.require` here.
val encoded = codec.encode(tlvs).require
// Decoding tlvs that we just encoded is safe as well.
// This encoding/decoding step ensures that the resulting tlvs are ordered.
val genericTlvs = vector(genericTlv).decode(encoded).require.value
val nonceKey = ByteVector("LnAll".getBytes) ++ encoded.bytes
def previousPowerOfTwo(n: Int): Int = {
var p = 1
while (p < n) {
p = p << 1
}
p >> 1
}
def merkleTree(i: Int, j: Int): ByteVector32 = {
val (a, b) = if (j - i == 1) {
val tlv = genericTlv.encode(genericTlvs(i)).require.bytes
(hash(ByteVector("LnLeaf".getBytes), tlv), hash(nonceKey, tlv))
} else {
val k = i + previousPowerOfTwo(j - i)
(merkleTree(i, k), merkleTree(k, j))
}
if (LexicographicalOrdering.isLessThan(a, b)) {
hash(ByteVector("LnBranch".getBytes), a ++ b)
} else {
hash(ByteVector("LnBranch".getBytes), b ++ a)
}
}
merkleTree(0, genericTlvs.length)
}
private def hash(tag: String, msg: ByteVector): ByteVector32 = {
ByteVector.encodeAscii(tag) match {
case Right(bytes) => hash(bytes, msg)
case Left(e) => throw e // NB: the tags we use are hard-coded, so we know they're always ASCII
}
}
private def hash(tag: ByteVector, msg: ByteVector): ByteVector32 = {
val tagHash = Crypto.sha256(tag)
Crypto.sha256(tagHash ++ tagHash ++ msg)
}
def signSchnorr(tag: String, msg: ByteVector32, key: PrivateKey): ByteVector64 = {
val h = hash(tag, msg)
// NB: we don't add auxiliary random data to keep signatures deterministic.
ByteVector64(ByteVector(Secp256k1JvmKt.getSecpk256k1.signSchnorr(h.toArray, key.value.toArray, null)))
}
def verifySchnorr(tag: String, msg: ByteVector32, signature: ByteVector64, publicKey: ByteVector32): Boolean = {
val h = hash(tag, msg)
Secp256k1JvmKt.getSecpk256k1.verifySchnorr(signature.toArray, h.toArray, publicKey.toArray)
}
def xOnlyPublicKey(publicKey: PublicKey): ByteVector32 = ByteVector32(publicKey.value.drop(1))
/** We often need to remove the signature field to compute the merkle root. */
def removeSignature[T <: Bolt12Tlv](records: TlvStream[T]): TlvStream[T] = {
TlvStream(records.records.filter { case _: Signature => false case _ => true }, records.unknown)
}
}

View File

@ -104,7 +104,7 @@ object TlvCodecs {
/** Length-prefixed truncated uint16 (1 to 3 bytes unsigned integer). */ /** Length-prefixed truncated uint16 (1 to 3 bytes unsigned integer). */
val ltu16: Codec[Int] = variableSizeBytes(uint8, tu16) val ltu16: Codec[Int] = variableSizeBytes(uint8, tu16)
private def validateGenericTlv(g: GenericTlv): Attempt[GenericTlv] = { private def validateUnknownTlv(g: GenericTlv): Attempt[GenericTlv] = {
if (g.tag < TLV_TYPE_HIGH_RANGE && g.tag.toBigInt % 2 == 0) { if (g.tag < TLV_TYPE_HIGH_RANGE && g.tag.toBigInt % 2 == 0) {
Attempt.Failure(Err("unknown even tlv type")) Attempt.Failure(Err("unknown even tlv type"))
} else { } else {
@ -112,7 +112,9 @@ object TlvCodecs {
} }
} }
val genericTlv: Codec[GenericTlv] = (("tag" | varint) :: variableSizeBytesLong(varintoverflow, bytes)).as[GenericTlv].exmap(validateGenericTlv, validateGenericTlv) val genericTlv: Codec[GenericTlv] = (("tag" | varint) :: variableSizeBytesLong(varintoverflow, bytes)).as[GenericTlv]
private val unknownTlv = genericTlv.exmap(validateUnknownTlv, validateUnknownTlv)
private def tag[T <: Tlv](codec: DiscriminatorCodec[T, UInt64], record: Either[GenericTlv, T]): UInt64 = record match { private def tag[T <: Tlv](codec: DiscriminatorCodec[T, UInt64], record: Either[GenericTlv, T]): UInt64 = record match {
case Left(generic) => generic.tag case Left(generic) => generic.tag
@ -140,7 +142,7 @@ object TlvCodecs {
* @param codec codec used for the tlv records contained in the stream. * @param codec codec used for the tlv records contained in the stream.
* @tparam T stream namespace. * @tparam T stream namespace.
*/ */
def tlvStream[T <: Tlv](codec: DiscriminatorCodec[T, UInt64]): Codec[TlvStream[T]] = list(discriminatorFallback(genericTlv, codec)).exmap( def tlvStream[T <: Tlv](codec: DiscriminatorCodec[T, UInt64]): Codec[TlvStream[T]] = list(discriminatorFallback(unknownTlv, codec)).exmap(
records => validateStream(codec, records), records => validateStream(codec, records),
(stream: TlvStream[T]) => { (stream: TlvStream[T]) => {
val records = (stream.records.map(Right(_)) ++ stream.unknown.map(Left(_))).toList val records = (stream.records.map(Right(_)) ++ stream.unknown.map(Left(_))).toList

View File

@ -171,19 +171,19 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers {
test("Bolt 11 invoice") { test("Bolt 11 invoice") {
val ref = "lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy" val ref = "lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy"
val pr = Invoice.fromString(ref) val pr = Invoice.fromString(ref).get
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional"},"unknown":[]},"routingInfo":[]}""" JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional"},"unknown":[]},"routingInfo":[]}"""
} }
test("Bolt 11 invoice with routing hints") { test("Bolt 11 invoice with routing hints") {
val ref = "lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an" val ref = "lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an"
val pr = Invoice.fromString(ref) val pr = Invoice.fromString(ref).get
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}""" JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}"""
} }
test("Bolt 11 invoice with metadata") { test("Bolt 11 invoice with metadata") {
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0" val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0"
val pr = Invoice.fromString(ref) val pr = Invoice.fromString(ref).get
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"var_onion_optin":"mandatory","payment_secret":"mandatory","option_payment_metadata":"mandatory"},"unknown":[]},"routingInfo":[]}""" JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"var_onion_optin":"mandatory","payment_secret":"mandatory","option_payment_metadata":"mandatory"},"unknown":[]},"routingInfo":[]}"""
} }

View File

@ -21,7 +21,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, Mil
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _} import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _}
import fr.acinq.eclair.payment.Bolt11Invoice._ import fr.acinq.eclair.payment.Bolt11Invoice._
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, Feature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32} import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32}
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import scodec.DecodeResult import scodec.DecodeResult
import scodec.bits._ import scodec.bits._
@ -132,7 +132,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad") { test("Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad") {
val ref = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql" val ref = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lnbc") assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt.isEmpty) assert(invoice.amount_opt.isEmpty)
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -148,7 +148,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("Please send $3 for a cup of coffee to the same peer, within 1 minute") { test("Please send $3 for a cup of coffee to the same peer, within 1 minute") {
val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh" val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lnbc") assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt === Some(250000000 msat)) assert(invoice.amount_opt === Some(250000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -163,7 +163,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within one minute") { test("Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within one minute") {
val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr" val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lnbc") assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt === Some(250000000 msat)) assert(invoice.amount_opt === Some(250000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -178,7 +178,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("Now send $24 for an entire list of things (hashed)") { test("Now send $24 for an entire list of things (hashed)") {
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44" val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lnbc") assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.amount_opt === Some(2000000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -193,7 +193,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP") { test("The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP") {
val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8" val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lntb") assert(invoice.prefix == "lntb")
assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.amount_opt === Some(2000000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -208,7 +208,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") { test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") {
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0" val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lnbc") assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.amount_opt === Some(2000000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -227,7 +227,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX") { test("On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX") {
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym" val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lnbc") assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.amount_opt === Some(2000000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -242,7 +242,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") { test("On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") {
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse" val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lnbc") assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.amount_opt === Some(2000000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -257,7 +257,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") { test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") {
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc" val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lnbc") assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.amount_opt === Some(2000000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -273,7 +273,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12") { test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12") {
val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0" val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lnbc") assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.amount_opt === Some(2000000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -298,7 +298,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
) )
for (ref <- refs) { for (ref <- refs) {
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix === "lnbc") assert(invoice.prefix === "lnbc")
assert(invoice.amount_opt === Some(2500000000L msat)) assert(invoice.amount_opt === Some(2500000000L msat))
assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -317,7 +317,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") {
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix === "lnbc") assert(invoice.prefix === "lnbc")
assert(invoice.amount_opt === Some(2500000000L msat)) assert(invoice.amount_opt === Some(2500000000L msat))
assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -336,7 +336,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") { test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") {
val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz" val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix === "lnbc") assert(invoice.prefix === "lnbc")
assert(invoice.amount_opt === Some(967878534 msat)) assert(invoice.amount_opt === Some(967878534 msat))
assert(invoice.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f") assert(invoice.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f")
@ -354,7 +354,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") { test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") {
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc" val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.prefix == "lnbc") assert(invoice.prefix == "lnbc")
assert(invoice.amount_opt === Some(1000000000 msat)) assert(invoice.amount_opt === Some(1000000000 msat))
assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
@ -386,7 +386,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
"lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x" "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x"
) )
for (ref <- refs) { for (ref <- refs) {
assertThrows[Exception](Bolt11Invoice.fromString(ref)) assert(Bolt11Invoice.fromString(ref).isFailure)
} }
} }
@ -403,7 +403,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
val invoice = Bolt11Invoice(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec) val invoice = Bolt11Invoice(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec)
assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(18)) assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(18))
val serialized = invoice.toString val serialized = invoice.toString
val pr1 = Bolt11Invoice.fromString(serialized) val Success(pr1) = Bolt11Invoice.fromString(serialized)
assert(invoice == pr1) assert(invoice == pr1)
} }
@ -421,7 +421,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
signature = ByteVector.empty).sign(priv) signature = ByteVector.empty).sign(priv)
val serialized = invoice.toString val serialized = invoice.toString
val pr1 = Bolt11Invoice.fromString(serialized) val Success(pr1) = Bolt11Invoice.fromString(serialized)
val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u } val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u }
} }
@ -451,12 +451,12 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
test("accept uppercase invoices") { test("accept uppercase invoices") {
val input = "lntb1500n1pwxx94fsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5q3xzmwuvxpkyhz6pvg3fcfxz0259kgh367qazj62af9rs0pw07dsdpa2fjkzep6yp58garswvaz7tmvd9nksarwd9hxw6n0w4kx2tnrdakj7grfwvs8wcqzysxqr23swwl9egjej7rvvt9zdxrtpy8xuu6cckdwajfccmtz7n90ea34k3j595w77pt69s5dx5a46f4k4w5avtvjkc4l4rm8n4xmk7fe3pms3pspdd032j" val input = "lntb1500n1pwxx94fsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5q3xzmwuvxpkyhz6pvg3fcfxz0259kgh367qazj62af9rs0pw07dsdpa2fjkzep6yp58garswvaz7tmvd9nksarwd9hxw6n0w4kx2tnrdakj7grfwvs8wcqzysxqr23swwl9egjej7rvvt9zdxrtpy8xuu6cckdwajfccmtz7n90ea34k3j595w77pt69s5dx5a46f4k4w5avtvjkc4l4rm8n4xmk7fe3pms3pspdd032j"
assert(Bolt11Invoice.fromString(input.toUpperCase()).toString == input) assert(Bolt11Invoice.fromString(input.toUpperCase()).get.toString == input)
} }
test("Pay 1 BTC without multiplier") { test("Pay 1 BTC without multiplier") {
val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58" val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58"
val invoice = Bolt11Invoice.fromString(ref) val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.amount_opt === Some(100000000000L msat)) assert(invoice.amount_opt === Some(100000000000L msat))
assert(features2bits(invoice.features) === BitVector.empty) assert(features2bits(invoice.features) === BitVector.empty)
} }
@ -487,7 +487,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
for ((features, res) <- featureBits) { for ((features, res) <- featureBits) {
val invoice = createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features) val invoice = createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features)
assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.invoiceFeatures().areSupported(invoice.features)) === res) assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.invoiceFeatures().areSupported(invoice.features)) === res)
assert(Bolt11Invoice.fromString(invoice.toString) === invoice) assert(Bolt11Invoice.fromString(invoice.toString).get === invoice)
} }
} }
@ -514,17 +514,15 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
assert(invoice.features === Features(PaymentSecret -> Mandatory, VariableLengthOnion -> Mandatory)) assert(invoice.features === Features(PaymentSecret -> Mandatory, VariableLengthOnion -> Mandatory))
assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory)))
val pr1 = Bolt11Invoice.fromString(invoice.toString) val Success(pr1) = Bolt11Invoice.fromString(invoice.toString)
assert(pr1.paymentSecret === invoice.paymentSecret) assert(pr1.paymentSecret === invoice.paymentSecret)
val pr2 = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") val Success(pr2) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl")
assert(!pr2.features.hasFeature(PaymentSecret, Some(Mandatory))) assert(!pr2.features.hasFeature(PaymentSecret, Some(Mandatory)))
assert(pr2.paymentSecret === None) assert(pr2.paymentSecret === None)
// An invoice that sets the payment secret feature bit must provide a payment secret. // An invoice that sets the payment secret feature bit must provide a payment secret.
assertThrows[IllegalArgumentException]( assert(Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7").isFailure)
Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7")
)
// A multi-part invoice must use a payment secret. // A multi-part invoice must use a payment secret.
assertThrows[IllegalArgumentException]( assertThrows[IllegalArgumentException](
@ -544,7 +542,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
assert(pr2.features.hasFeature(BasicMultiPartPayment)) assert(pr2.features.hasFeature(BasicMultiPartPayment))
assert(pr2.features.hasFeature(TrampolinePaymentPrototype)) assert(pr2.features.hasFeature(TrampolinePaymentPrototype))
val pr3 = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") val Success(pr3) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl")
assert(!pr3.features.hasFeature(TrampolinePaymentPrototype)) assert(!pr3.features.hasFeature(TrampolinePaymentPrototype))
} }
@ -616,8 +614,9 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
) )
for ((req, nodeId) <- requests) { for ((req, nodeId) <- requests) {
assert(Bolt11Invoice.fromString(req).nodeId === nodeId) val Success(invoice) = Bolt11Invoice.fromString(req)
assert(Bolt11Invoice.fromString(req).toString === req) assert(invoice.nodeId === nodeId)
assert(invoice.toString === req)
} }
} }
@ -625,7 +624,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty) assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty)
val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures()) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures())
assert(invoice.features === Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory)) assert(invoice.features === Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory))
assert(Bolt11Invoice.fromString(invoice.toString) === invoice) assert(Bolt11Invoice.fromString(invoice.toString).get === invoice)
} }
test("Invoices can't have high features"){ test("Invoices can't have high features"){

View File

@ -0,0 +1,349 @@
/*
* Copyright 2022 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.payment
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion}
import fr.acinq.eclair.payment.Bolt12Invoice.signatureTag
import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceRequestTlvCodec, invoiceTlvCodec}
import fr.acinq.eclair.wire.protocol.Offers._
import fr.acinq.eclair.wire.protocol.{GenericTlv, Offers, TlvStream}
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshiLong, TimestampSecond, TimestampSecondLong, UInt64, randomBytes32, randomBytes64, randomKey}
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits._
import scala.concurrent.duration.DurationInt
import scala.util.Success
class Bolt12InvoiceSpec extends AnyFunSuite {
def signInvoice(invoice: Bolt12Invoice, key: PrivateKey): Bolt12Invoice = {
val tlvs = Offers.removeSignature(invoice.records)
val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), key)
val signedInvoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None)
assert(signedInvoice.checkSignature())
signedInvoice
}
test("check invoice signature") {
val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32())
val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain)
val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain)
val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty)
assert(invoice.isValidFor(offer, request))
assert(invoice.checkSignature())
assert(!invoice.checkRefundSignature())
assert(Bolt12Invoice.fromString(invoice.toString).get.toString === invoice.toString)
// changing signature makes check fail
val withInvalidSignature = Bolt12Invoice(TlvStream(invoice.records.records.map { case Signature(_) => Signature(randomBytes64()) case x => x }, invoice.records.unknown), None)
assert(!withInvalidSignature.checkSignature())
assert(!withInvalidSignature.isValidFor(offer, request))
assert(!withInvalidSignature.checkRefundSignature())
// changing fields makes the signature invalid
val withModifiedUnknownTlv = Bolt12Invoice(invoice.records.copy(unknown = Seq(GenericTlv(UInt64(7), hex"ade4"))), None)
assert(!withModifiedUnknownTlv.checkSignature())
assert(!withModifiedUnknownTlv.isValidFor(offer, request))
assert(!withModifiedUnknownTlv.checkRefundSignature())
val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(amount) => Amount(amount + 100.msat) case x => x }, invoice.records.unknown), None)
assert(!withModifiedAmount.checkSignature())
assert(!withModifiedAmount.isValidFor(offer, request))
assert(!withModifiedAmount.checkRefundSignature())
}
test("check that invoice matches offer") {
val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32())
val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain)
val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain)
val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty)
assert(invoice.isValidFor(offer, request))
assert(!invoice.isValidFor(Offer(None, "test offer", randomKey().publicKey, Features.empty, chain), request))
// amount must match the offer
val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(9000 msat) case x => x }.toSeq), None), nodeKey)
assert(!withOtherAmount.isValidFor(offer, request))
// description must match the offer, may have appended info
val withOtherDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("other description") case x => x }.toSeq), None), nodeKey)
assert(!withOtherDescription.isValidFor(offer, request))
val withExtendedDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("test offer + more") case x => x }.toSeq), None), nodeKey)
assert(withExtendedDescription.isValidFor(offer, request))
// nodeId must match the offer
val otherNodeKey = randomKey()
val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case NodeId(_) => NodeId(otherNodeKey.publicKey) case x => x }.toSeq), None), otherNodeKey)
assert(!withOtherNodeId.isValidFor(offer, request))
// offerId must match the offer
val withOtherOfferId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x }.toSeq), None), nodeKey)
assert(!withOtherOfferId.isValidFor(offer, request))
// issuer must match the offer
val withOtherIssuer = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records ++ Seq(Issuer("spongebob"))), None), nodeKey)
assert(!withOtherIssuer.isValidFor(offer, request))
}
test("check that invoice matches invoice request") {
val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32())
val offer = Offer(Some(15000 msat), "test offer", nodeKey.publicKey, Features(VariableLengthOnion -> Mandatory), chain)
val request = InvoiceRequest(offer, 15000 msat, 1, Features(VariableLengthOnion -> Mandatory), payerKey, chain)
assert(request.quantity_opt === None) // when paying for a single item, the quantity field must not be present
val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional))
assert(invoice.isValidFor(offer, request))
val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case FeaturesTlv(_) => FeaturesTlv(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory)) case x => x }.toSeq), None), nodeKey)
assert(!withInvalidFeatures.isValidFor(offer, request))
val withAmountTooBig = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(20000 msat) case x => x }.toSeq), None), nodeKey)
assert(!withAmountTooBig.isValidFor(offer, request))
val withQuantity = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ Quantity(2)), None), nodeKey)
assert(!withQuantity.isValidFor(offer, request))
val withOtherPayerKey = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case PayerKey(_) => PayerKey(randomBytes32()) case x => x }.toSeq), None), nodeKey)
assert(!withOtherPayerKey.isValidFor(offer, request))
val withPayerNote = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerNote("I am Batman")), None), nodeKey)
assert(!withPayerNote.isValidFor(offer, request))
val withPayerInfo = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerInfo(hex"010203040506")), None), nodeKey)
assert(!withPayerInfo.isValidFor(offer, request))
// Invoice request with more details about the payer.
val requestWithPayerDetails = {
val tlvs: Seq[InvoiceRequestTlv] = Seq(
OfferId(offer.offerId),
Amount(15000 msat),
PayerKey(payerKey.publicKey),
PayerInfo(hex"010203040506"),
PayerNote("I am Batman"),
FeaturesTlv(Features(VariableLengthOnion -> Mandatory))
)
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey)
InvoiceRequest(TlvStream(tlvs :+ Signature(signature)))
}
val withPayerDetails = Bolt12Invoice(offer, requestWithPayerDetails, randomBytes32(), nodeKey, Features.empty)
assert(withPayerDetails.isValidFor(offer, requestWithPayerDetails))
assert(!withPayerDetails.isValidFor(offer, request))
val withOtherPayerInfo = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerInfo(_) => PayerInfo(hex"deadbeef") case x => x }.toSeq), None), nodeKey)
assert(!withOtherPayerInfo.isValidFor(offer, requestWithPayerDetails))
assert(!withOtherPayerInfo.isValidFor(offer, request))
val withOtherPayerNote = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerNote(_) => PayerNote("Or am I Bruce Wayne?") case x => x }.toSeq), None), nodeKey)
assert(!withOtherPayerNote.isValidFor(offer, requestWithPayerDetails))
assert(!withOtherPayerNote.isValidFor(offer, request))
}
test("check invoice expiry") {
val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32())
val offer = Offer(Some(5000 msat), "test offer", nodeKey.publicKey, Features.empty, chain)
val request = InvoiceRequest(offer, 5000 msat, 1, Features.empty, payerKey, chain)
val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty)
assert(!invoice.isExpired())
assert(invoice.isValidFor(offer, request))
val expiredInvoice1 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(0 unixsec) case x => x }), None), nodeKey)
assert(expiredInvoice1.isExpired())
assert(!expiredInvoice1.isValidFor(offer, request)) // when an invoice is expired, we mark it as invalid as well
val expiredInvoice2 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(TimestampSecond.now() - 2000) case x => x } ++ Seq(RelativeExpiry(1800))), None), nodeKey)
assert(expiredInvoice2.isExpired())
assert(!expiredInvoice2.isValidFor(offer, request)) // when an invoice is expired, we mark it as invalid as well
}
test("check chain compatibility") {
val amount = 5000 msat
val (nodeKey, payerKey) = (randomKey(), randomKey())
val (chain1, chain2) = (randomBytes32(), randomBytes32())
val offerBtc = Offer(Some(amount), "bitcoin offer", nodeKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
val requestBtc = InvoiceRequest(offerBtc, amount, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
val invoiceImplicitBtc = {
val tlvs: Seq[InvoiceTlv] = Seq(
CreatedAt(TimestampSecond.now()),
PaymentHash(Crypto.sha256(randomBytes32())),
OfferId(offerBtc.offerId),
NodeId(nodeKey.publicKey),
Amount(amount),
Description(offerBtc.description),
PayerKey(payerKey.publicKey)
)
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None)
}
assert(invoiceImplicitBtc.isValidFor(offerBtc, requestBtc))
val invoiceExplicitBtc = {
val tlvs: Seq[InvoiceTlv] = Seq(
Chain(Block.LivenetGenesisBlock.hash),
CreatedAt(TimestampSecond.now()),
PaymentHash(Crypto.sha256(randomBytes32())),
OfferId(offerBtc.offerId),
NodeId(nodeKey.publicKey),
Amount(amount),
Description(offerBtc.description),
PayerKey(payerKey.publicKey)
)
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None)
}
assert(invoiceExplicitBtc.isValidFor(offerBtc, requestBtc))
val invoiceOtherChain = {
val tlvs: Seq[InvoiceTlv] = Seq(
Chain(chain1),
CreatedAt(TimestampSecond.now()),
PaymentHash(Crypto.sha256(randomBytes32())),
OfferId(offerBtc.offerId),
NodeId(nodeKey.publicKey),
Amount(amount),
Description(offerBtc.description),
PayerKey(payerKey.publicKey)
)
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None)
}
assert(!invoiceOtherChain.isValidFor(offerBtc, requestBtc))
val offerOtherChains = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(amount), Description("testnets offer"), NodeId(nodeKey.publicKey))))
val requestOtherChains = InvoiceRequest(offerOtherChains, amount, 1, Features.empty, payerKey, chain1)
val invoiceOtherChains = {
val tlvs: Seq[InvoiceTlv] = Seq(
Chain(chain1),
CreatedAt(TimestampSecond.now()),
PaymentHash(Crypto.sha256(randomBytes32())),
OfferId(offerOtherChains.offerId),
NodeId(nodeKey.publicKey),
Amount(amount),
Description(offerOtherChains.description),
PayerKey(payerKey.publicKey)
)
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None)
}
assert(invoiceOtherChains.isValidFor(offerOtherChains, requestOtherChains))
val invoiceInvalidOtherChain = {
val tlvs: Seq[InvoiceTlv] = Seq(
Chain(chain2),
CreatedAt(TimestampSecond.now()),
PaymentHash(Crypto.sha256(randomBytes32())),
OfferId(offerOtherChains.offerId),
NodeId(nodeKey.publicKey),
Amount(amount),
Description(offerOtherChains.description),
PayerKey(payerKey.publicKey)
)
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None)
}
assert(!invoiceInvalidOtherChain.isValidFor(offerOtherChains, requestOtherChains))
val invoiceMissingChain = signInvoice(Bolt12Invoice(TlvStream(invoiceOtherChains.records.records.filter { case Chain(_) => false case _ => true }), None), nodeKey)
assert(!invoiceMissingChain.isValidFor(offerOtherChains, requestOtherChains))
}
test("decode invoice") {
val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05")
val payerKey = PrivateKey(hex"7dd30ec116470c5f7f00af2c7e84968e28cdb43083b33ee832decbe73ec07f1a")
val Success(offer) = Offer.decode("lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2pd3xzumfvvsx7enxv4epug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly")
assert(offer.amount === Some(100_000 msat))
assert(offer.nodeIdXOnly === xOnlyPublicKey(nodeKey.publicKey))
assert(offer.chains === Seq(Block.TestnetGenesisBlock.hash))
val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.TestnetGenesisBlock.hash)
val Success(invoice) = Bolt12Invoice.fromString("lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyp53zuupqkwxpmdq0tjg58ntat5ujpejlvyn92r0l5xzh4wru8e5zzqrqxr2qzstvfshx6tryphkven9wgxqq83qk6msaxhyk0n9xnajs5swehp24wndvvn0ftppu7363evzc9uwrnujvg95tuyy05nqkcdetsaljgq4u6789jllc54qrpjrzzn3c38dj3tscu5qgcs2y4lj5gqlvq50uu7sce478j3j0l599nxfs6svx2cfefgn4a0675893wtzuckqfwlcrcq0qspa9zynlpdk9zzechehkemgaksklylxhr7yfjfx6h696th327nm4nsf52xzq0ukchx69g00c4vvk6kzc5jyklneyy05l9tef7a5jcjn5")
assert(!invoice.isExpired())
assert(invoice.isValidFor(offer, request))
}
test("decode invoice with quantity") {
val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05")
val payerKey = PrivateKey(hex"94c7a21a11efa16c5f73b093dc136d9525e2ff40ea7a958c43c1f6004bf6a676")
val Success(offer) = Offer.decode("lno1pqpzwyq2pf382mrtyphkven9wgtqzqgcqy9pug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly")
assert(offer.amount === Some(10_000 msat))
assert(offer.nodeIdXOnly === xOnlyPublicKey(nodeKey.publicKey))
assert(offer.chains === Seq(Block.LivenetGenesisBlock.hash))
val request = InvoiceRequest(offer, 50_000 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
val Success(invoice) = Bolt12Invoice.fromString("lni1qss8u47nw2lsgml7fy4jaqwph9f8cl83zfrrhxccvh6076avqzzzv4qgqtp4qzs2vf6kc6eqdanxvetjpsqpug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsulysqzpfxyrat02l8wtgtwuc4h5hw6dxhn0hcpdrtu3dpejfjdlw9h4j3nppxc2qyvg9z0lf2yq7wl9ygd6td4cj7whp3ye4cfxrtu7zq4r2mc0mcdspk3duzv7d0stqyh0upuq8sgr44r7aaluwqfw8pkd9f3cgk7ae2l8rkexznhegr0p7w4mlhvfkvlnr5k2lnw0hhsf6ckys3sst7kng5p7m2pxlvdxl3tan809vkk75j")
assert(!invoice.isExpired())
assert(invoice.amount === 50_000.msat)
assert(invoice.quantity === Some(5))
assert(invoice.isValidFor(offer, request))
}
test("decode invalid invoice") {
val testCases = Seq(
// Missing amount.
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3c2pf6x2um5yphkven9wgxqq83q2hfdphr3r8x07ej0z0swprnll58z4jlw36wye7kw63ssm95ru8qjvgxj40x4favsm7uue24lhleg7gng2r69g2plgwxm8xmpuw7wmnyh455qgcs29g3j5g8vpnxqvnzwu0sgqc20hdllxzr4qnpu0zge6drn8p3galht8f62uncyqyrhddpcla8pprdxwdkmjmutya0kvnrpeqjqa75sr02wff0le52ydckr8ww09gg4jyvxyma903fhh6v8t4edftg7vw6qz7h0tz20p5wq",
// Missing node id.
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqzvgxj40x4favsm7uue24lhleg7gng2r69g2plgwxm8xmpuw7wmnyh455qgcs29g3j5g8vpnxqvnzwu0sgqc20hdllxzr4qnpu0zge6drn8p3galht8f62uncypc7vg95clf4z8fklzua72nhavtjp5qp0u7pemgpecypn6q809qr5733c9y0thf6fdnegxleeupddgzgsyszrktay6cjv9cld3wzlw5pq",
// Missing payment hash.
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9qzxyz32y0cyqhfql7g2dp8w0s5xc0ccgelq4hrnkgmxxltvdq95rqzpf4e68j60h6dysm3evhnu4rwtrqp3dnekmk9sxklw267axtj6zangxtnfx2pq",
// Missing description.
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcsrqqrcs9t5ksm3c3nn8lve838c8q3ell6r32e0hga8zvlt8dgcgdj6p7rsfxyrf2hn257kgdlwwv42lmlu50yf59paz59ql58rdnnds7808dejt662qyvg9z5ge2yrkqenqxf38w8cyqv98mkllnpp6sfs783yvax3ensc5wlm4n5a9wfuzqjraxg7gnskte8m9lzn2j5r55a4n3nhhfnflzd5953tau0h2auztf9und4psz6p34wx4vsjxwyvc33lezqvm208ntdczqneylzznt0cg",
// Missing creation date.
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9gswcrxvqexyaclqsps5lwml7vy82pxrc7y3n568xwrz3mlwkwn54e8sgp22vcpqylq4zcxep5faeysey8ucu6wrfgffphlt457rd9f7tlpnyltlqz0yd9eqjcehyu3zwvear2e4ksx32t3qtek9gucrephdmsk0",
// Missing signature.
"lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9qzxyz32yv4zpmqvesrycnhruzqxznam0lessagyc0rcjxwnguecv280a6e6wjhy",
)
for (testCase <- testCases) {
assert(Bolt12Invoice.fromString(testCase).isFailure, testCase)
}
}
test("encode/decode invoice with many fields") {
val chain = Block.TestnetGenesisBlock.hash
val offerId = ByteVector32.fromValidHex("8bc5978de5d625c90136dfa896a8a02cef33c5457027684687e3f98e0cfca4f0")
val amount = 123456 msat
val description = "invoice with many fields"
val features = Features[Feature](Features.VariableLengthOnion -> FeatureSupport.Mandatory)
val issuer = "acinq.co"
val nodeKey = PrivateKey(hex"998cf8ecab46f949bb960813b79d3317cabf4193452a211795cd8af1b9a25d90")
val quantity = 57
val payerKey = ByteVector32.fromValidHex("8faadd71b1f78b16265e5b061b9d2b88891012dc7ad38626eeaaa2a271615a65")
val payerNote = "I'm the king"
val payerInfo = hex"a9eb6e526eac59cd9b89fb20"
val createdAt = TimestampSecond(1654654654L)
val paymentHash = ByteVector32.fromValidHex("51951d4c53c904035f0b293dc9df1c0e7967213430ae07a5f3e134cd33325341")
val relativeExpiry = 3600
val cltv = CltvExpiryDelta(123)
val fallbacks = Seq(FallbackAddress(4, hex"123d56f8"), FallbackAddress(6, hex"eb3adc68945ef601"))
val replaceInvoice = ByteVector32.fromValidHex("71ad033e5f42068225608770fa7672505449425db543a1f9c23bf03657aa37c1")
val tlvs = TlvStream[InvoiceTlv](Seq(
Chain(chain),
OfferId(offerId),
Amount(amount),
Description(description),
FeaturesTlv(features),
Issuer(issuer),
NodeId(nodeKey.publicKey),
Quantity(quantity),
PayerKey(payerKey),
PayerNote(payerNote),
PayerInfo(payerInfo),
CreatedAt(createdAt),
PaymentHash(paymentHash),
RelativeExpiry(relativeExpiry),
Cltv(cltv),
Fallbacks(fallbacks),
ReplaceInvoice(replaceInvoice)
), Seq(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex"")))
val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), nodeKey)
val invoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None)
assert(invoice.toString === "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsq9qgv93kjmn39e3k783qc3nnudswp67znrydjtv7ta56c9cpc0nmjmv7rszs568gqdz3w77zqqfeycsgl2kawxcl0zckye09kpsmn54c3zgsztw845uxymh24g4zw9s45ef8p3yjwmfqw35x2grtd9hxw2qyv2sqd032ypge282v20ysgq6lpv5nmjwlrs88jeepxsc2upa970snfnfnxff5ztqzpcgzuqsq0vcpgpcyqqzpy02klq9svqqgavadc6y5tmmqzvsv484ku5nw43vumxuflvsrsgr345pnuh6zq6pz2cy8wra8vujs23y5yhd4gwslns3m7qm9023hc8cyq6knwqxzve9r5kpufq3szuhn9f437cj05az5kqnsl9wefhfnwzenf5z68qh5jj48rmku97u0gzdm2wlkuwrylpvqfttdtw972cwdteal6qfhqvqsyqlaqyusq")
val Success(codedDecoded) = Bolt12Invoice.fromString(invoice.toString)
assert(codedDecoded.chain === chain)
assert(codedDecoded.offerId === Some(offerId))
assert(codedDecoded.amount === amount)
assert(codedDecoded.description === Left(description))
assert(codedDecoded.features === features)
assert(codedDecoded.issuer === Some(issuer))
assert(codedDecoded.nodeId.value.drop(1) === nodeKey.publicKey.value.drop(1))
assert(codedDecoded.quantity === Some(quantity))
assert(codedDecoded.payerKey === Some(payerKey))
assert(codedDecoded.payerNote === Some(payerNote))
assert(codedDecoded.payerInfo === Some(payerInfo))
assert(codedDecoded.createdAt === createdAt)
assert(codedDecoded.paymentHash === paymentHash)
assert(codedDecoded.relativeExpiry === relativeExpiry.seconds)
assert(codedDecoded.minFinalCltvExpiryDelta === cltv)
assert(codedDecoded.fallbacks === Some(fallbacks))
assert(codedDecoded.replaceInvoice === Some(replaceInvoice))
assert(codedDecoded.records.unknown.toSet === Set(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex"")))
}
}

View File

@ -0,0 +1,283 @@
/*
* Copyright 2022 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.wire.protocol
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion}
import fr.acinq.eclair.wire.protocol.OfferCodecs.invoiceRequestTlvCodec
import fr.acinq.eclair.wire.protocol.Offers._
import fr.acinq.eclair.{Features, MilliSatoshiLong, randomBytes32, randomKey}
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits.{ByteVector, HexStringSyntax}
import scala.util.Success
class OffersSpec extends AnyFunSuite {
val nodeId = ByteVector32(hex"4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605")
test("sign and check offer") {
val key = randomKey()
val offer = Offer(Some(100_000 msat), "test offer", key.publicKey, Features(VariableLengthOnion -> Mandatory), Block.LivenetGenesisBlock.hash)
assert(offer.signature.isEmpty)
val signedOffer = offer.sign(key)
assert(signedOffer.checkSignature())
}
test("invoice request is signed") {
val sellerKey = randomKey()
val offer = Offer(Some(100_000 msat), "test offer", sellerKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
val payerKey = randomKey()
val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
assert(request.checkSignature())
}
test("basic offer") {
val encoded = "lno1pg257enxv4ezqcneype82um50ynhxgrwdajx283qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczs"
val offer = Offer.decode(encoded).get
assert(offer.amount.isEmpty)
assert(offer.signature.isEmpty)
assert(offer.description === "Offer by rusty's node")
assert(offer.nodeIdXOnly === nodeId)
}
test("basic signed offer") {
val encodedSigned = "lno1pg257enxv4ezqcneype82um50ynhxgrwdajx283qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqs85ck65ycmkdk92smwt9zuewdzfe7v4aavvaz5kgv9mkk63v3s0ge0f099kssh3yc95qztx504hu92hnx8ctzhtt08pgk0texz0509tk"
val Success(signedOffer) = Offer.decode(encodedSigned)
assert(signedOffer.checkSignature())
assert(signedOffer.amount.isEmpty)
assert(signedOffer.description === "Offer by rusty's node")
assert(signedOffer.nodeIdXOnly === nodeId)
}
test("offer with amount and quantity") {
val encoded = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykq5"
val Success(offer) = Offer.decode(encoded)
assert(offer.amount === Some(50 msat))
assert(offer.signature.isEmpty)
assert(offer.description === "50msat multi-quantity offer")
assert(offer.nodeIdXOnly === nodeId)
assert(offer.issuer === Some("rustcorp.com.au"))
assert(offer.quantityMin === Some(1))
}
test("signed offer with amount and quantity") {
val encodedSigned = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykqhcypjju7unu05vav8yvhn27lztf46k9gqlga8uvu4uq62kpuywnu6me8srgh2q7puczukr8arectaapfl5d4rd6uc7st7tnqf0ttx39n40s"
val Success(signedOffer) = Offer.decode(encodedSigned)
assert(signedOffer.checkSignature())
assert(signedOffer.amount === Some(50 msat))
assert(signedOffer.description === "50msat multi-quantity offer")
assert(signedOffer.nodeIdXOnly === nodeId)
assert(signedOffer.issuer === Some("rustcorp.com.au"))
assert(signedOffer.quantityMin === Some(1))
}
test("decode invalid offer") {
val testCases = Seq(
"lno1pgxx7enxv4e8xgrjda3kkgg", // missing node id
"lno1rcsdhss957tylk58rmly849jupnmzs52ydhxhl8fgz7994xkf2hnwhg", // missing description
)
for (testCase <- testCases) {
assert(Offer.decode(testCase).isFailure)
}
}
def signInvoiceRequest(request: InvoiceRequest, key: PrivateKey): InvoiceRequest = {
val tlvs = removeSignature(request.records)
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(tlvs, invoiceRequestTlvCodec), key)
val signedRequest = InvoiceRequest(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))))
assert(signedRequest.checkSignature())
signedRequest
}
test("check that invoice request matches offer") {
val offer = Offer(Some(2500 msat), "basic offer", randomKey().publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
val payerKey = randomKey()
val request = InvoiceRequest(offer, 2500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
assert(request.isValidFor(offer))
val biggerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case Amount(_) => Amount(3000 msat) case x => x })), payerKey)
assert(biggerAmount.isValidFor(offer))
val lowerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case Amount(_) => Amount(2000 msat) case x => x })), payerKey)
assert(!lowerAmount.isValidFor(offer))
val otherOfferId = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x })), payerKey)
assert(!otherOfferId.isValidFor(offer))
val withQuantity = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Quantity(1)))), payerKey)
assert(!withQuantity.isValidFor(offer))
}
test("check that invoice request matches offer (with features)") {
val offer = Offer(Some(2500 msat), "offer with features", randomKey().publicKey, Features(VariableLengthOnion -> Optional), Block.LivenetGenesisBlock.hash)
val payerKey = randomKey()
val request = InvoiceRequest(offer, 2500 msat, 1, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional), payerKey, Block.LivenetGenesisBlock.hash)
assert(request.isValidFor(offer))
val withoutFeatures = InvoiceRequest(offer, 2500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
assert(withoutFeatures.isValidFor(offer))
val otherFeatures = InvoiceRequest(offer, 2500 msat, 1, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory), payerKey, Block.LivenetGenesisBlock.hash)
assert(!otherFeatures.isValidFor(offer))
}
test("check that invoice request matches offer (without amount)") {
val offer = Offer(None, "offer without amount", randomKey().publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
val payerKey = randomKey()
val request = InvoiceRequest(offer, 500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
assert(request.isValidFor(offer))
val withoutAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.filter { case Amount(_) => false case _ => true })), payerKey)
assert(!withoutAmount.isValidFor(offer))
}
test("check that invoice request matches offer (chain compatibility)") {
{
val offer = Offer(TlvStream(Seq(Amount(100 msat), Description("offer without chains"), NodeId(randomKey().publicKey))))
val payerKey = randomKey()
val request = {
val tlvs: Seq[InvoiceRequestTlv] = Seq(
OfferId(offer.offerId),
Amount(100 msat),
PayerKey(payerKey.publicKey),
FeaturesTlv(Features.empty)
)
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey)
InvoiceRequest(TlvStream(tlvs :+ Signature(signature)))
}
assert(request.isValidFor(offer))
val withDefaultChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Chain(Block.LivenetGenesisBlock.hash)))), payerKey)
assert(withDefaultChain.isValidFor(offer))
val otherChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Chain(Block.TestnetGenesisBlock.hash)))), payerKey)
assert(!otherChain.isValidFor(offer))
}
{
val (chain1, chain2) = (randomBytes32(), randomBytes32())
val offer = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(100 msat), Description("offer with chains"), NodeId(randomKey().publicKey))))
val payerKey = randomKey()
val request1 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain1)
assert(request1.isValidFor(offer))
val request2 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain2)
assert(request2.isValidFor(offer))
val noChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.filter { case Chain(_) => false case _ => true })), payerKey)
assert(!noChain.isValidFor(offer))
val otherChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.map { case Chain(_) => Chain(Block.LivenetGenesisBlock.hash) case x => x })), payerKey)
assert(!otherChain.isValidFor(offer))
}
}
test("check that invoice request matches offer (multiple items)") {
val offer = Offer(TlvStream(
Amount(500 msat),
Description("offer for multiple items"),
NodeId(randomKey().publicKey),
QuantityMin(3),
QuantityMax(10),
))
val payerKey = randomKey()
val request = InvoiceRequest(offer, 1600 msat, 3, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
assert(request.records.get[Quantity].nonEmpty)
assert(request.isValidFor(offer))
val invalidAmount = InvoiceRequest(offer, 2400 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
assert(!invalidAmount.isValidFor(offer))
val tooFewItems = InvoiceRequest(offer, 1000 msat, 2, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
assert(!tooFewItems.isValidFor(offer))
val tooManyItems = InvoiceRequest(offer, 5500 msat, 11, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
assert(!tooManyItems.isValidFor(offer))
}
test("decode invoice request") {
val encoded = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypz8xu3xwsqpar9dd26lgrrvc7s63ljt0pgh6ag2utv5udez7n2mjzqzz47qcqczqgqzqqgzycsv2tmjgzc5l546aldq699wj9pdusvfred97l352p4aa862vqvzw5p8pdyjqctdyppxzardv9hrypx74klwluzqd0rqgeew2uhuagttuv6aqwklvm0xmlg52lfnagzw8ygt0wrtnv2tsx69m6tgug7njaw5ypa5fn369n9yzc87v02rqccj9h04dxf3nzc"
val Success(request) = InvoiceRequest.decode(encoded)
assert(request.amount === Some(5500 msat))
assert(request.offerId === ByteVector32(hex"4473722674001e8cad6ab5f40c6cc7a1a8fe4b78517d750ae2d94e3722f4d5b9"))
assert(request.quantity === 2)
assert(request.features === Features(VariableLengthOnion -> Optional, BasicMultiPartPayment -> Optional))
assert(request.records.get[Chain].nonEmpty)
assert(request.chain === Block.LivenetGenesisBlock.hash)
assert(request.payerKey === ByteVector32(hex"c52f7240b14fd2baefda0d14ae9142de41891e5a5f7e34506bde9f4a60182750"))
assert(request.payerInfo === Some(hex"deadbeef"))
assert(request.payerNote === Some("I am Batman"))
assert(request.encode() === encoded)
}
test("decode invalid invoice request") {
val testCases = Seq(
// Missing offer id.
"lnr1pqpp8zqvqqnzqq7pw52tqj6pj2mar5cgkmnt9xe3tj40nxc3pp95xml2e8v432ny7pq957u2v4r5cjxfmxtwk9qfu99hftq2ek48pz6c2ywynajha03ut4ffjf34htxxxp668dqd9jwvz2eal6up5mjfe4ad8ndccrtpkkke0g",
// Missing payer key.
"lnr1qss0h356hn94473j5yls8q3w4gkzu9j8rrach3hgms4ks8aumsx29vsgqgfcsrqq7pq957u2v4r5cjxfmxtwk9qfu99hftq2ek48pz6c2ywynajha03ut4ffjf34htxxxp668dqd9jwvz2eal6up5mjfe4ad8ndccrtpkkke0g",
// Missing signature.
"lnr1qss0h356hn94473j5yls8q3w4gkzu9j8rrach3hgms4ks8aumsx29vsgqgfcsrqqycsq8st4zjcyksvjklgaxz9ku6efkv2u4tuekyggfdpkl6kfm9v25eq",
)
for (testCase <- testCases) {
assert(InvoiceRequest.decode(testCase).isFailure)
}
}
test("compute merkle tree root") {
import scodec.Codec
import scodec.codecs.list
case class TestCase(tlvs: ByteVector, count: Int, expected: ByteVector32)
val testCases = Seq(
// Official test vectors.
TestCase(hex"010203e8", 1, ByteVector32(hex"aa0aa0f694c85492ac459c1de9831a37682985f5e840ecc9b1e28eece7dc5236")),
TestCase(hex"010203e8 02080000010000020003", 2, ByteVector32(hex"013b756ed73554cbc4dd3d90f363cb7cba6d8a279465a21c464e582b173ff502")),
TestCase(hex"010203e8 02080000010000020003 03310266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c0351800000000000000010000000000000002", 3, ByteVector32(hex"016fcda3b6f9ca30b35936877ca591fa101365a761a1453cfd9436777d593656")),
TestCase(hex"0603555344 080203e8 0a0f313055534420657665727920646179 141072757374792e6f7a6c6162732e6f7267 1a020101 1e204b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605", 6, ByteVector32(hex"7cef68df49fd9222bed0138ca5603da06464b2f523ea773bc4edcb6bd07966e7")),
// Additional test vectors.
TestCase(hex"010100", 1, ByteVector32(hex"c8112b235945b06a11995bf69956a93ff0403c28de35bd33b4714da1b6239ebb")),
TestCase(hex"010100 020100", 2, ByteVector32(hex"8271d606bea3ef49e59d610585317edfc6c53d8d1afd763731919d9a7d70a7d9")),
TestCase(hex"010100 020100 030100", 3, ByteVector32(hex"c7eff290817749d87eede061d5335559e8211769e651a2ee5c5e7d2ddd655236")),
TestCase(hex"010100 020100 030100 040100", 4, ByteVector32(hex"57883ef2f1e8df4a23e6f0a2e3acda2ed0b11e00ef2d39fe1caa2d71d7273c37")),
TestCase(hex"010100 020100 030100 040100 050100", 5, ByteVector32(hex"85b74f254eced46c525a5369c52f86f249a41f6f6ccb3c918ffe4025ea22d8b6")),
TestCase(hex"010100 020100 030100 040100 050100 060100", 6, ByteVector32(hex"6cf27da8a67b7cb199dd1824017cb008bd22bf1d57273a8c4544c5408275dc2d")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100", 7, ByteVector32(hex"2a038f022b51b1b969563679a22eb167ef603d5b2cb2d0dbe86dc4d2f48d8c6e")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100", 8, ByteVector32(hex"8ddbe97f6ed2e2a4a43e828e350f9cb6679b7d5f16837273cf0e6f7da342fa19")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100", 9, ByteVector32(hex"8050bed857ff7929a24251e3a517fc14f46fb0f02e6719cb9d53087f7f047f6d")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100", 10, ByteVector32(hex"e22aa818e746ff9613e6cccc99ebce257d93c35736b168b6b478d6f3762f56ce")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100", 11, ByteVector32(hex"626e3159cec72534155ccf691a84ab68da89e6cd679a118c70a28fd1f1bb10cc")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100", 12, ByteVector32(hex"f659da1c839d99a2c6b104d179ee44ffe3a9eaa55831de3c612c8c293c27401b")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100", 13, ByteVector32(hex"c165756a94718d19e66ff7b581347699009a9e17805e16cb6ba94c034c7dc757")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100", 14, ByteVector32(hex"573b85bbceacbf1b189412858ac6573e923bbf0c9cfdc37d37757996f6086208")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100", 15, ByteVector32(hex"84a3088fe74b82ee82a9415d48fdfad8dc6a286fec8e6fcdcefcf0bc02f3256e")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100", 16, ByteVector32(hex"a686f116fce33e43fa875fec2253c71694a0324e1ce7640ed1070b0cc3a14cc1")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100", 17, ByteVector32(hex"fbee87d6726c8b67a8d2e2bff92b13d0b1d9188f9d42af2d3afefceaafa6f3e5")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100", 18, ByteVector32(hex"5004f619c426b01e57c480a84d5dcdc3a70b4bf575ec573be60c3a75ed978b72")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100", 19, ByteVector32(hex"6f0a5e59f1fa5dc6c12ed3bbe0eb91c818b22a8d011d5a2160462c59e6158a58")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100", 20, ByteVector32(hex"e43f00c262e4578c5ed4413ab340e79cb8b258241b7c52550b7307f7b9c4d645")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100", 21, ByteVector32(hex"e46776637883bae1a62cbfb621c310c13e6c522092954e08d74c08328d53f035")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100", 22, ByteVector32(hex"813ebe9f07638005abbe270f11ae2749a5b9b0c5cf89a305598303a38f5f2da5")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100", 23, ByteVector32(hex"fdd7b779192dcbadb5695303e2bcee0fc175428278bdbfa4b4445251df6c9450")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100", 24, ByteVector32(hex"33c92b7820742d094548328ee3bfdf29bf3fe1f971171dcd2a6da0f185dceddb")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100", 25, ByteVector32(hex"888da09f5ba1b8e431b3ab1db62fca94c0cbbec6b145012d9308d20f68571ff2")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100", 26, ByteVector32(hex"a87cdc040109b855d81f13af4a6f57cdb7e31252eeb83bc03518fdd6dd81ec18")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100", 27, ByteVector32(hex"9829715a0d8cbb5c080de53704f274aa4da3590e8338d57ce99ab491d7a44e76")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100", 28, ByteVector32(hex"ce8bac7c3d10b528d59d5f391bf36bb6acd65b4bb3cbd0a769488e3b451b2c26")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100", 29, ByteVector32(hex"88d29ac3e4ae8761058af4b1baaa873ec4f76822166f8dfc2888bcbb51212130")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100", 30, ByteVector32(hex"b013259fe32c6eaf88d2b3b2d01350e5505bcc0fcdcdc7c360e5644fe827424d")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100", 31, ByteVector32(hex"1c60489269d312c2ea94c637936e38a968d2900cab6c5544db091aa8b3bb5176")),
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100 200100", 32, ByteVector32(hex"e0d88bd7685ffd55e0de4e45e190e7e6bf1ecc0a7d1a32fbdaa6b1b27e8bc37b")),
)
testCases.foreach {
case TestCase(tlvStream, tlvCount, expectedRoot) =>
val genericTlvStream: Codec[TlvStream[GenericTlv]] = list(TlvCodecs.genericTlv).xmap(tlvs => TlvStream(tlvs), tlvs => tlvs.records.toList)
val tlvs = genericTlvStream.decode(tlvStream.bits).require.value
assert(tlvs.records.size === tlvCount)
val root = Offers.rootHash(tlvs, genericTlvStream)
assert(root === expectedRoot)
}
}
}

View File

@ -44,7 +44,7 @@ object FormParamExtractors {
implicit val sha256HashesUnmarshaller: Unmarshaller[String, List[ByteVector32]] = listUnmarshaller(bin => ByteVector32.fromValidHex(bin)) implicit val sha256HashesUnmarshaller: Unmarshaller[String, List[ByteVector32]] = listUnmarshaller(bin => ByteVector32.fromValidHex(bin))
implicit val bolt11Unmarshaller: Unmarshaller[String, Bolt11Invoice] = Unmarshaller.strict { rawRequest => Bolt11Invoice.fromString(rawRequest) } implicit val bolt11Unmarshaller: Unmarshaller[String, Bolt11Invoice] = Unmarshaller.strict { rawRequest => Bolt11Invoice.fromString(rawRequest).get }
implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => ShortChannelId(str) } implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => ShortChannelId(str) }

View File

@ -824,7 +824,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
test("'getreceivedinfo' 2") { test("'getreceivedinfo' 2") {
val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending)
val eclair = mock[Eclair] val eclair = mock[Eclair]
val pending = randomBytes32() val pending = randomBytes32()
eclair.receivedInfo(pending)(any) returns Future.successful(Some(defaultPayment)) eclair.receivedInfo(pending)(any) returns Future.successful(Some(defaultPayment))
@ -844,7 +844,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
test("'getreceivedinfo' 3") { test("'getreceivedinfo' 3") {
val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending)
val eclair = mock[Eclair] val eclair = mock[Eclair]
val expired = randomBytes32() val expired = randomBytes32()
eclair.receivedInfo(expired)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Expired))) eclair.receivedInfo(expired)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Expired)))
@ -864,7 +864,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
test("'getreceivedinfo' 4") { test("'getreceivedinfo' 4") {
val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending)
val eclair = mock[Eclair] val eclair = mock[Eclair]
val received = randomBytes32() val received = randomBytes32()
eclair.receivedInfo(received)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Received(42 msat, TimestampMilli(1633439543777L))))) eclair.receivedInfo(received)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Received(42 msat, TimestampMilli(1633439543777L)))))
@ -989,7 +989,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
test("'findroute' method response should support nodeId, shortChannelId and full formats") { test("'findroute' method response should support nodeId, shortChannelId and full formats") {
val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh"
val invoice = Invoice.fromString(serializedInvoice) val invoice = Invoice.fromString(serializedInvoice).get
val mockChannelUpdate1 = ChannelUpdate( val mockChannelUpdate1 = ChannelUpdate(
signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"),

View File

@ -72,7 +72,7 @@
<akka.version>2.6.18</akka.version> <akka.version>2.6.18</akka.version>
<akka.http.version>10.2.7</akka.http.version> <akka.http.version>10.2.7</akka.http.version>
<sttp.version>3.4.1</sttp.version> <sttp.version>3.4.1</sttp.version>
<bitcoinlib.version>0.22</bitcoinlib.version> <bitcoinlib.version>0.23</bitcoinlib.version>
<guava.version>31.1-jre</guava.version> <guava.version>31.1-jre</guava.version>
<kamon.version>2.4.6</kamon.version> <kamon.version>2.4.6</kamon.version>
</properties> </properties>