mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-23 06:35:11 +01:00
Updated offers spec (#2386)
- copy offer into invoice request and invoice request into invoice - change signature calculation
This commit is contained in:
parent
530b3c206f
commit
f95f087e0b
28 changed files with 859 additions and 883 deletions
|
@ -56,6 +56,11 @@ trait InitFeature extends Feature
|
|||
trait NodeFeature extends Feature
|
||||
/** Feature that should be advertised in invoices. */
|
||||
trait InvoiceFeature extends Feature
|
||||
/** Feature that should be advertised in Bolt 11 invoices. */
|
||||
trait Bolt11Feature extends InvoiceFeature
|
||||
/** Feature that should be advertised in Bolt 12 invoices. */
|
||||
trait Bolt12Feature extends InvoiceFeature
|
||||
|
||||
/**
|
||||
* Feature negotiated when opening a channel that will apply for all of the channel's lifetime.
|
||||
* This doesn't include features that can be safely activated/deactivated without impacting the channel's operation such
|
||||
|
@ -101,6 +106,10 @@ case class Features[T <: Feature](activated: Map[T, FeatureSupport], unknown: Se
|
|||
|
||||
def invoiceFeatures(): Features[InvoiceFeature] = Features(activated.collect { case (f: InvoiceFeature, s) => (f, s) }, unknown)
|
||||
|
||||
def bolt11Features(): Features[Bolt11Feature] = Features(activated.collect { case (f: Bolt11Feature, s) => (f, s) }, unknown)
|
||||
|
||||
def bolt12Features(): Features[Bolt12Feature] = Features(activated.collect { case (f: Bolt12Feature, s) => (f, s) }, unknown)
|
||||
|
||||
def unscoped(): Features[Feature] = Features[Feature](activated.collect { case (f, s) => (f: Feature, s) }, unknown)
|
||||
|
||||
def add(feature: T, support: FeatureSupport): Features[T] = copy(activated = activated + (feature -> support))
|
||||
|
@ -185,7 +194,7 @@ object Features {
|
|||
val mandatory = 6
|
||||
}
|
||||
|
||||
case object VariableLengthOnion extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||
case object VariableLengthOnion extends Feature with InitFeature with NodeFeature with Bolt11Feature {
|
||||
val rfcName = "var_onion_optin"
|
||||
val mandatory = 8
|
||||
}
|
||||
|
@ -200,12 +209,12 @@ object Features {
|
|||
val mandatory = 12
|
||||
}
|
||||
|
||||
case object PaymentSecret extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||
case object PaymentSecret extends Feature with InitFeature with NodeFeature with Bolt11Feature {
|
||||
val rfcName = "payment_secret"
|
||||
val mandatory = 14
|
||||
}
|
||||
|
||||
case object BasicMultiPartPayment extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||
case object BasicMultiPartPayment extends Feature with InitFeature with NodeFeature with Bolt11Feature with Bolt12Feature {
|
||||
val rfcName = "basic_mpp"
|
||||
val mandatory = 16
|
||||
}
|
||||
|
@ -225,7 +234,7 @@ object Features {
|
|||
val mandatory = 22
|
||||
}
|
||||
|
||||
case object RouteBlinding extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||
case object RouteBlinding extends Feature with InitFeature with NodeFeature with Bolt11Feature {
|
||||
val rfcName = "option_route_blinding"
|
||||
val mandatory = 24
|
||||
}
|
||||
|
@ -255,7 +264,7 @@ object Features {
|
|||
val mandatory = 46
|
||||
}
|
||||
|
||||
case object PaymentMetadata extends Feature with InvoiceFeature {
|
||||
case object PaymentMetadata extends Feature with Bolt11Feature {
|
||||
val rfcName = "option_payment_metadata"
|
||||
val mandatory = 48
|
||||
}
|
||||
|
@ -276,13 +285,13 @@ object Features {
|
|||
// The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted,
|
||||
// we will introduce a new version of trampoline that will work in parallel to this legacy one, until we can safely
|
||||
// deprecate it.
|
||||
case object TrampolinePaymentPrototype extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||
case object TrampolinePaymentPrototype extends Feature with InitFeature with NodeFeature with Bolt11Feature {
|
||||
val rfcName = "trampoline_payment_prototype"
|
||||
val mandatory = 148
|
||||
}
|
||||
|
||||
// TODO: @remyers update feature bits once spec-ed (currently reserved here: https://github.com/lightning/bolts/pull/989)
|
||||
case object AsyncPaymentPrototype extends Feature with InitFeature with InvoiceFeature {
|
||||
case object AsyncPaymentPrototype extends Feature with InitFeature with Bolt11Feature {
|
||||
val rfcName = "async_payment_prototype"
|
||||
val mandatory = 152
|
||||
}
|
||||
|
|
|
@ -429,8 +429,8 @@ object InvoiceSerializer extends MinimalSerializer({
|
|||
)),
|
||||
JField("blindedPaths", JArray(p.blindedPaths.map(path => {
|
||||
JObject(List(
|
||||
JField("introductionNodeId", JString(path.introductionNodeId.toString())),
|
||||
JField("blindedNodeIds", JArray(path.blindedNodes.map(n => JString(n.blindedPublicKey.toString())).toList))
|
||||
JField("introductionNodeId", JString(path.route.introductionNodeId.toString())),
|
||||
JField("blindedNodeIds", JArray(path.route.blindedNodes.map(n => JString(n.blindedPublicKey.toString())).toList))
|
||||
))
|
||||
}).toList)),
|
||||
JField("createdAt", JLong(p.createdAt.toLong)),
|
||||
|
|
|
@ -19,7 +19,7 @@ package fr.acinq.eclair.payment
|
|||
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto}
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32}
|
||||
import fr.acinq.eclair.{Bolt11Feature, CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32}
|
||||
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
|
||||
import scodec.codecs.{list, ubyte}
|
||||
import scodec.{Codec, Err}
|
||||
|
@ -92,7 +92,7 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat
|
|||
|
||||
lazy val minFinalCltvExpiryDelta: CltvExpiryDelta = tags.collectFirst { case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta }.getOrElse(DEFAULT_MIN_CLTV_EXPIRY_DELTA)
|
||||
|
||||
lazy val features: Features[InvoiceFeature] = tags.collectFirst { case f: InvoiceFeatures => f.features.invoiceFeatures() }.getOrElse(Features.empty[InvoiceFeature])
|
||||
override lazy val features: Features[InvoiceFeature] = tags.collectFirst { case f: InvoiceFeatures => f.features.invoiceFeatures() }.getOrElse(Features.empty)
|
||||
|
||||
/**
|
||||
* @return the hash of this payment invoice
|
||||
|
@ -142,7 +142,7 @@ object Bolt11Invoice {
|
|||
Block.LivenetGenesisBlock.hash -> "lnbc"
|
||||
)
|
||||
|
||||
val defaultFeatures: Features[InvoiceFeature] = Features((Features.VariableLengthOnion, FeatureSupport.Mandatory), (Features.PaymentSecret, FeatureSupport.Mandatory))
|
||||
val defaultFeatures: Features[Bolt11Feature] = Features((Features.VariableLengthOnion, FeatureSupport.Mandatory), (Features.PaymentSecret, FeatureSupport.Mandatory))
|
||||
|
||||
def apply(chainHash: ByteVector32,
|
||||
amount: Option[MilliSatoshi],
|
||||
|
@ -156,7 +156,7 @@ object Bolt11Invoice {
|
|||
timestamp: TimestampSecond = TimestampSecond.now(),
|
||||
paymentSecret: ByteVector32 = randomBytes32(),
|
||||
paymentMetadata: Option[ByteVector] = None,
|
||||
features: Features[InvoiceFeature] = defaultFeatures): Bolt11Invoice = {
|
||||
features: Features[Bolt11Feature] = defaultFeatures): Bolt11Invoice = {
|
||||
require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret")
|
||||
require(!features.hasFeature(Features.RouteBlinding), "bolt11 invoices cannot use route blinding")
|
||||
val prefix = prefixes(chainHash)
|
||||
|
|
|
@ -18,13 +18,12 @@ package fr.acinq.eclair.payment
|
|||
|
||||
import fr.acinq.bitcoin.Bech32
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto}
|
||||
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding
|
||||
import fr.acinq.eclair.wire.protocol.OfferTypes._
|
||||
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{InvalidTlvPayload, MissingRequiredTlv}
|
||||
import fr.acinq.eclair.wire.protocol.{OfferCodecs, OfferTypes, TlvStream}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond, UInt64}
|
||||
import fr.acinq.eclair.{Bolt12Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, TimestampSecond, UInt64}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -39,57 +38,36 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice {
|
|||
|
||||
import Bolt12Invoice._
|
||||
|
||||
val amount: MilliSatoshi = records.get[Amount].map(_.amount).get
|
||||
val invoiceRequest: InvoiceRequest = InvoiceRequest.validate(filterInvoiceRequestFields(records)).toOption.get
|
||||
|
||||
val amount: MilliSatoshi = records.get[InvoiceAmount].map(_.amount).get
|
||||
override val amount_opt: Option[MilliSatoshi] = Some(amount)
|
||||
override val nodeId: Crypto.PublicKey = records.get[NodeId].get.publicKey
|
||||
override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash
|
||||
override val description: Either[String, ByteVector32] = Left(records.get[Description].get.description)
|
||||
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)
|
||||
val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash)
|
||||
val offerId: Option[ByteVector32] = records.get[OfferId].map(_.offerId)
|
||||
val blindedPaths: Seq[RouteBlinding.BlindedRoute] = records.get[Paths].get.paths
|
||||
val blindedPathsInfo: Seq[PaymentInfo] = records.get[PaymentPathsInfo].get.paymentInfo
|
||||
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)
|
||||
override val nodeId: Crypto.PublicKey = records.get[InvoiceNodeId].get.nodeId
|
||||
override val paymentHash: ByteVector32 = records.get[InvoicePaymentHash].get.hash
|
||||
override val description: Either[String, ByteVector32] = Left(invoiceRequest.offer.description)
|
||||
override val createdAt: TimestampSecond = records.get[InvoiceCreatedAt].get.timestamp
|
||||
override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[InvoiceRelativeExpiry].map(_.seconds).getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS)
|
||||
override val features: Features[InvoiceFeature] = {
|
||||
val f = records.get[InvoiceFeatures].map(_.features.invoiceFeatures()).getOrElse(Features.empty)
|
||||
// We add invoice features that are implicitly required for Bolt 12 (the spec doesn't allow explicitly setting them).
|
||||
f.add(Features.VariableLengthOnion, FeatureSupport.Mandatory).add(Features.RouteBlinding, FeatureSupport.Mandatory)
|
||||
}
|
||||
val blindedPaths: Seq[PaymentBlindedRoute] = records.get[InvoicePaths].get.paths.zip(records.get[InvoiceBlindedPay].get.paymentInfo).map { case (route, info) => PaymentBlindedRoute(route, info) }
|
||||
val fallbacks: Option[Seq[FallbackAddress]] = records.get[InvoiceFallbacks].map(_.addresses)
|
||||
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 = {
|
||||
nodeId == offer.nodeId &&
|
||||
checkSignature() &&
|
||||
offerId.contains(request.offerId) &&
|
||||
request.chain == chain &&
|
||||
def isValidFor(request: InvoiceRequest): Boolean = {
|
||||
invoiceRequest.unsigned == request.unsigned &&
|
||||
nodeId == invoiceRequest.offer.nodeId &&
|
||||
!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
|
||||
}
|
||||
request.amount.forall(_ == amount) &&
|
||||
Features.areCompatible(request.features, features.bolt12Features()) &&
|
||||
checkSignature()
|
||||
}
|
||||
|
||||
def checkSignature(): Boolean = {
|
||||
verifySchnorr(signatureTag("signature"), rootHash(OfferTypes.removeSignature(records), OfferCodecs.invoiceTlvCodec), signature, OfferTypes.xOnlyPublicKey(nodeId))
|
||||
verifySchnorr(signatureTag, rootHash(OfferTypes.removeSignature(records), OfferCodecs.invoiceTlvCodec), signature, nodeId)
|
||||
}
|
||||
|
||||
override def toString: String = {
|
||||
|
@ -103,65 +81,55 @@ case class PaymentBlindedRoute(route: Sphinx.RouteBlinding.BlindedRoute, payment
|
|||
|
||||
object Bolt12Invoice {
|
||||
val hrp = "lni"
|
||||
val signatureTag: ByteVector = ByteVector(("lightning" + "invoice" + "signature").getBytes)
|
||||
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
|
||||
* @param paths the blinded paths to use to pay the invoice
|
||||
*/
|
||||
def apply(offer: Offer,
|
||||
request: InvoiceRequest,
|
||||
def apply(request: InvoiceRequest,
|
||||
preimage: ByteVector32,
|
||||
nodeKey: PrivateKey,
|
||||
minFinalCltvExpiryDelta: CltvExpiryDelta,
|
||||
features: Features[InvoiceFeature],
|
||||
invoiceExpiry: FiniteDuration,
|
||||
features: Features[Bolt12Feature],
|
||||
paths: Seq[PaymentBlindedRoute]): Bolt12Invoice = {
|
||||
require(request.amount.nonEmpty || offer.amount.nonEmpty)
|
||||
val amount = request.amount.orElse(offer.amount.map(_ * request.quantity)).get
|
||||
val tlvs: Seq[InvoiceTlv] = Seq(
|
||||
Some(Chain(request.chain)),
|
||||
Some(OfferId(offer.offerId)),
|
||||
Some(Amount(amount)),
|
||||
Some(Description(offer.description)),
|
||||
if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None,
|
||||
Some(Paths(paths.map(_.route))),
|
||||
Some(PaymentPathsInfo(paths.map(_.paymentInfo))),
|
||||
offer.issuer.map(Issuer),
|
||||
Some(NodeId(nodeKey.publicKey)),
|
||||
request.quantity_opt.map(Quantity),
|
||||
Some(PayerKey(request.payerKey)),
|
||||
request.payerNote.map(PayerNote),
|
||||
Some(CreatedAt(TimestampSecond.now())),
|
||||
Some(PaymentHash(Crypto.sha256(preimage))),
|
||||
Some(Cltv(minFinalCltvExpiryDelta)),
|
||||
request.payerInfo.map(PayerInfo),
|
||||
request.replaceInvoice.map(ReplaceInvoice),
|
||||
require(request.amount.nonEmpty || request.offer.amount.nonEmpty)
|
||||
val amount = request.amount.orElse(request.offer.amount.map(_ * request.quantity)).get
|
||||
val tlvs: Seq[InvoiceTlv] = removeSignature(request.records).records.toSeq ++ Seq(
|
||||
Some(InvoicePaths(paths.map(_.route))),
|
||||
Some(InvoiceBlindedPay(paths.map(_.paymentInfo))),
|
||||
Some(InvoiceCreatedAt(TimestampSecond.now())),
|
||||
Some(InvoiceRelativeExpiry(invoiceExpiry.toSeconds)),
|
||||
Some(InvoicePaymentHash(Crypto.sha256(preimage))),
|
||||
Some(InvoiceAmount(amount)),
|
||||
if (!features.isEmpty) Some(InvoiceFeatures(features.unscoped())) else None,
|
||||
Some(InvoiceNodeId(nodeKey.publicKey)),
|
||||
).flatten
|
||||
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), OfferCodecs.invoiceTlvCodec), nodeKey)
|
||||
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)))
|
||||
val signature = signSchnorr(signatureTag, rootHash(TlvStream(tlvs, request.records.unknown), OfferCodecs.invoiceTlvCodec), nodeKey)
|
||||
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature), request.records.unknown))
|
||||
}
|
||||
|
||||
def validate(records: TlvStream[InvoiceTlv]): Either[InvalidTlvPayload, Bolt12Invoice] = {
|
||||
if (records.get[Amount].isEmpty) return Left(MissingRequiredTlv(UInt64(8)))
|
||||
if (records.get[Description].isEmpty) return Left(MissingRequiredTlv(UInt64(10)))
|
||||
if (records.get[Paths].isEmpty) return Left(MissingRequiredTlv(UInt64(16)))
|
||||
if (records.get[PaymentPathsInfo].map(_.paymentInfo.length) != records.get[Paths].map(_.paths.length)) return Left(MissingRequiredTlv(UInt64(18)))
|
||||
if (records.get[NodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(30)))
|
||||
if (records.get[CreatedAt].isEmpty) return Left(MissingRequiredTlv(UInt64(40)))
|
||||
if (records.get[PaymentHash].isEmpty) return Left(MissingRequiredTlv(UInt64(42)))
|
||||
InvoiceRequest.validate(filterInvoiceRequestFields(records)).fold(
|
||||
invalidTlvPayload => return Left(invalidTlvPayload),
|
||||
_ -> ()
|
||||
)
|
||||
if (records.get[InvoiceAmount].isEmpty) return Left(MissingRequiredTlv(UInt64(170)))
|
||||
if (records.get[InvoicePaths].forall(_.paths.isEmpty)) return Left(MissingRequiredTlv(UInt64(160)))
|
||||
if (records.get[InvoiceBlindedPay].map(_.paymentInfo.length) != records.get[InvoicePaths].map(_.paths.length)) return Left(MissingRequiredTlv(UInt64(162)))
|
||||
if (records.get[InvoiceNodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(176)))
|
||||
if (records.get[InvoiceCreatedAt].isEmpty) return Left(MissingRequiredTlv(UInt64(164)))
|
||||
if (records.get[InvoicePaymentHash].isEmpty) return Left(MissingRequiredTlv(UInt64(168)))
|
||||
if (records.get[Signature].isEmpty) return Left(MissingRequiredTlv(UInt64(240)))
|
||||
Right(Bolt12Invoice(records))
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -33,7 +33,6 @@ trait Invoice {
|
|||
def paymentHash: ByteVector32
|
||||
def description: Either[String, ByteVector32]
|
||||
def relativeExpiry: FiniteDuration
|
||||
def minFinalCltvExpiryDelta: CltvExpiryDelta
|
||||
def features: Features[InvoiceFeature]
|
||||
def isExpired(now: TimestampSecond = TimestampSecond.now()): Boolean = createdAt + relativeExpiry.toSeconds <= now
|
||||
def toString: String
|
||||
|
|
|
@ -35,10 +35,10 @@ import fr.acinq.eclair.payment._
|
|||
import fr.acinq.eclair.router.BlindedRouteCreation.{aggregatePaymentInfo, createBlindedRouteFromHops, createBlindedRouteWithoutHops}
|
||||
import fr.acinq.eclair.router.Router
|
||||
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams}
|
||||
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer}
|
||||
import fr.acinq.eclair.wire.protocol.OfferTypes.InvoiceRequest
|
||||
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload
|
||||
import fr.acinq.eclair.wire.protocol._
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampMilli, randomBytes32}
|
||||
import fr.acinq.eclair.{Bolt11Feature, CltvExpiryDelta, FeatureSupport, Features, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampMilli, randomBytes32}
|
||||
import scodec.bits.HexStringSyntax
|
||||
|
||||
import scala.concurrent.duration.DurationInt
|
||||
|
@ -108,9 +108,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
|
|||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
val desc = Left("Donation")
|
||||
val features = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) {
|
||||
Features[InvoiceFeature](Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
|
||||
Features[Bolt11Feature](Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
|
||||
} else {
|
||||
Features[InvoiceFeature](Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
|
||||
Features[Bolt11Feature](Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
|
||||
}
|
||||
// Insert a fake invoice and then restart the incoming payment handler
|
||||
val invoice = Bolt11Invoice(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.channelConf.minFinalExpiryDelta, paymentSecret = payload.paymentSecret, features = features)
|
||||
|
@ -268,24 +268,24 @@ object MultiPartHandler {
|
|||
/**
|
||||
* Use this message to create a Bolt 12 invoice to receive a payment for a given offer.
|
||||
*
|
||||
* @param nodeKey the key that will be used to sign the invoice, which may be different from our public nodeId.
|
||||
* @param offer the offer this invoice corresponds to.
|
||||
* @param nodeKey the private key corresponding to the offer node id. It will be used to sign the invoice
|
||||
* and may be different from our public nodeId.
|
||||
* @param invoiceRequest the request this invoice responds to.
|
||||
* @param routes routes that must be blinded and provided in the invoice.
|
||||
* @param router router actor.
|
||||
* @param paymentPreimage_opt payment preimage.
|
||||
*/
|
||||
case class ReceiveOfferPayment(nodeKey: PrivateKey,
|
||||
offer: Offer,
|
||||
invoiceRequest: InvoiceRequest,
|
||||
routes: Seq[ReceivingRoute],
|
||||
router: ActorRef,
|
||||
paymentPreimage_opt: Option[ByteVector32] = None,
|
||||
paymentType: String = PaymentType.Blinded) extends ReceivePayment {
|
||||
require(nodeKey.publicKey == invoiceRequest.offer.nodeId, "the node id of the invoice must be the same as the one from the offer")
|
||||
require(routes.forall(_.nodes.nonEmpty), "each route must have at least one node")
|
||||
require(offer.amount.nonEmpty || invoiceRequest.amount.nonEmpty, "an amount must be specified in the offer or in the invoice request")
|
||||
require(invoiceRequest.offer.amount.nonEmpty || invoiceRequest.amount.nonEmpty, "an amount must be specified in the offer or in the invoice request")
|
||||
|
||||
val amount = invoiceRequest.amount.orElse(offer.amount.map(_ * invoiceRequest.quantity)).get
|
||||
val amount = invoiceRequest.amount.orElse(invoiceRequest.offer.amount.map(_ * invoiceRequest.quantity)).get
|
||||
}
|
||||
|
||||
object CreateInvoiceActor {
|
||||
|
@ -301,16 +301,16 @@ object MultiPartHandler {
|
|||
case CreateInvoice(replyTo, receivePayment) =>
|
||||
val paymentPreimage = receivePayment.paymentPreimage_opt.getOrElse(randomBytes32())
|
||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
val featuresTrampolineOpt = if (nodeParams.enableTrampolinePayment) {
|
||||
nodeParams.features.invoiceFeatures().add(Features.TrampolinePaymentPrototype, FeatureSupport.Optional)
|
||||
} else {
|
||||
nodeParams.features.invoiceFeatures()
|
||||
}
|
||||
receivePayment match {
|
||||
case r: ReceiveStandardPayment =>
|
||||
Try {
|
||||
val expirySeconds = r.expirySeconds_opt.getOrElse(nodeParams.invoiceExpiry.toSeconds)
|
||||
val paymentMetadata = hex"2a"
|
||||
val featuresTrampolineOpt = if (nodeParams.enableTrampolinePayment) {
|
||||
nodeParams.features.bolt11Features().add(Features.TrampolinePaymentPrototype, FeatureSupport.Optional)
|
||||
} else {
|
||||
nodeParams.features.bolt11Features()
|
||||
}
|
||||
val invoice = Bolt11Invoice(
|
||||
nodeParams.chainHash,
|
||||
r.amount_opt,
|
||||
|
@ -349,21 +349,21 @@ object MultiPartHandler {
|
|||
} else {
|
||||
createBlindedRouteFromHops(dummyHops, pathId, nodeParams.channelConf.htlcMinimum, route.maxFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight))
|
||||
}
|
||||
val paymentInfo = aggregatePaymentInfo(r.amount, dummyHops)
|
||||
val paymentInfo = aggregatePaymentInfo(r.amount, dummyHops, nodeParams.channelConf.minFinalExpiryDelta)
|
||||
Future.successful((blindedRoute, paymentInfo, pathId))
|
||||
} else {
|
||||
implicit val timeout: Timeout = 10.seconds
|
||||
r.router.ask(Router.FinalizeRoute(Router.PredefinedNodeRoute(r.amount, route.nodes))).mapTo[Router.RouteResponse].map(routeResponse => {
|
||||
val clearRoute = routeResponse.routes.head
|
||||
val blindedRoute = createBlindedRouteFromHops(clearRoute.hops ++ dummyHops, pathId, nodeParams.channelConf.htlcMinimum, route.maxFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight))
|
||||
val paymentInfo = aggregatePaymentInfo(r.amount, clearRoute.hops ++ dummyHops)
|
||||
val paymentInfo = aggregatePaymentInfo(r.amount, clearRoute.hops ++ dummyHops, nodeParams.channelConf.minFinalExpiryDelta)
|
||||
(blindedRoute, paymentInfo, pathId)
|
||||
})
|
||||
}
|
||||
})).map(paths => {
|
||||
val invoiceFeatures = featuresTrampolineOpt.remove(Features.RouteBlinding).add(Features.RouteBlinding, FeatureSupport.Mandatory)
|
||||
val invoice = Bolt12Invoice(r.offer, r.invoiceRequest, paymentPreimage, r.nodeKey, nodeParams.channelConf.minFinalExpiryDelta, invoiceFeatures, paths.map { case (blindedRoute, paymentInfo, _) => PaymentBlindedRoute(blindedRoute.route, paymentInfo) })
|
||||
log.debug("generated invoice={} for offerId={}", invoice.toString, r.offer.offerId)
|
||||
val invoiceFeatures = nodeParams.features.bolt12Features()
|
||||
val invoice = Bolt12Invoice(r.invoiceRequest, paymentPreimage, r.nodeKey, nodeParams.invoiceExpiry, invoiceFeatures, paths.map { case (blindedRoute, paymentInfo, _) => PaymentBlindedRoute(blindedRoute.route, paymentInfo) })
|
||||
log.debug("generated invoice={} for offer={}", invoice.toString, r.invoiceRequest.offer.toString)
|
||||
nodeParams.db.payments.addIncomingBlindedPayment(invoice, paymentPreimage, paths.map { case (blindedRoute, _, pathId) => blindedRoute.lastBlinding -> pathId.bytes }.toMap, r.paymentType)
|
||||
invoice
|
||||
}).recover(exception => Status.Failure(exception)).pipeTo(replyTo)
|
||||
|
@ -441,7 +441,12 @@ object MultiPartHandler {
|
|||
}
|
||||
|
||||
private def validatePaymentCltv(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload)(implicit log: LoggingAdapter): Boolean = {
|
||||
val minExpiry = nodeParams.channelConf.minFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)
|
||||
val minExpiry = payload match {
|
||||
case _: FinalPayload.Standard => nodeParams.channelConf.minFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)
|
||||
// For blinded payments, the min-final-expiry-delta is included in the blinded path instead of being added
|
||||
// explicitly by the sender to their onion payload's expiry.
|
||||
case _: FinalPayload.Blinded => nodeParams.channelConf.minFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight.max(payload.expiry.blockHeight))
|
||||
}
|
||||
if (add.cltvExpiry < minExpiry) {
|
||||
log.warning("received payment with expiry too small for amount={} totalAmount={}", add.amountMsat, payload.totalAmount)
|
||||
false
|
||||
|
|
|
@ -259,8 +259,14 @@ object PaymentInitiator {
|
|||
def invoice: Invoice
|
||||
def recipientNodeId: PublicKey = invoice.nodeId
|
||||
def paymentHash: ByteVector32 = invoice.paymentHash
|
||||
def finalExpiry(nodeParams: NodeParams): CltvExpiry = nodeParams.paymentFinalExpiry.computeFinalExpiry(nodeParams.currentBlockHeight, invoice.minFinalCltvExpiryDelta)
|
||||
// @formatter:on
|
||||
def finalExpiry(nodeParams: NodeParams): CltvExpiry = {
|
||||
val minFinalCltvExpiryDelta = invoice match {
|
||||
case invoice: Bolt11Invoice => invoice.minFinalCltvExpiryDelta
|
||||
case _: Bolt12Invoice => CltvExpiryDelta(0)
|
||||
}
|
||||
nodeParams.paymentFinalExpiry.computeFinalExpiry(nodeParams.currentBlockHeight, minFinalCltvExpiryDelta)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -167,13 +167,13 @@ case class BlindedRecipient(nodeId: PublicKey,
|
|||
|
||||
object BlindedRecipient {
|
||||
def apply(invoice: Bolt12Invoice, totalAmount: MilliSatoshi, expiry: CltvExpiry, customTlvs: Seq[GenericTlv]): BlindedRecipient = {
|
||||
val blindedHops = invoice.blindedPaths.zip(invoice.blindedPathsInfo).map {
|
||||
case (route, info) =>
|
||||
val blindedHops = invoice.blindedPaths.map(
|
||||
path => {
|
||||
// We don't know the scids of channels inside the blinded route, but it's useful to have an ID to refer to a
|
||||
// given edge in the graph, so we create a dummy one for the duration of the payment attempt.
|
||||
val dummyId = ShortChannelId.generateLocalAlias()
|
||||
BlindedHop(dummyId, route, info)
|
||||
}
|
||||
BlindedHop(dummyId, path.route, path.paymentInfo)
|
||||
})
|
||||
BlindedRecipient(invoice.nodeId, invoice.features, totalAmount, expiry, blindedHops, customTlvs)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ import scodec.bits.ByteVector
|
|||
object BlindedRouteCreation {
|
||||
|
||||
/** Compute aggregated fees and expiry for a given route. */
|
||||
def aggregatePaymentInfo(amount: MilliSatoshi, hops: Seq[ChannelHop]): PaymentInfo = {
|
||||
val zeroPaymentInfo = PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty)
|
||||
def aggregatePaymentInfo(amount: MilliSatoshi, hops: Seq[ChannelHop], minFinalCltvExpiryDelta: CltvExpiryDelta): PaymentInfo = {
|
||||
val zeroPaymentInfo = PaymentInfo(0 msat, 0, minFinalCltvExpiryDelta, 0 msat, amount, Features.empty)
|
||||
hops.foldRight(zeroPaymentInfo) {
|
||||
case (channel, payInfo) =>
|
||||
val newFeeBase = MilliSatoshi((channel.params.relayFees.feeBase.toLong * 1_000_000 + payInfo.feeBase.toLong * (1_000_000 + channel.params.relayFees.feeProportionalMillionths) + 1_000_000 - 1) / 1_000_000)
|
||||
|
|
|
@ -135,9 +135,9 @@ object MessageOnionCodecs {
|
|||
private val onionTlvCodec = discriminated[OnionMessagePayloadTlv].by(varint)
|
||||
.typecase(UInt64(2), replyPathCodec)
|
||||
.typecase(UInt64(4), encryptedDataCodec)
|
||||
.typecase(UInt64(64), tlvField(OfferCodecs.invoiceRequestTlvCodec.as[InvoiceRequest]))
|
||||
.typecase(UInt64(66), tlvField(OfferCodecs.invoiceTlvCodec.as[Invoice]))
|
||||
.typecase(UInt64(68), tlvField(OfferCodecs.invoiceErrorTlvCodec.as[InvoiceError]))
|
||||
.typecase(UInt64(64), OfferCodecs.invoiceRequestCodec)
|
||||
.typecase(UInt64(66), OfferCodecs.invoiceCodec)
|
||||
.typecase(UInt64(68), OfferCodecs.invoiceErrorCodec)
|
||||
|
||||
val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete
|
||||
|
||||
|
|
|
@ -19,135 +19,159 @@ package fr.acinq.eclair.wire.protocol
|
|||
import fr.acinq.bitcoin.scalacompat.ByteVector32
|
||||
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute}
|
||||
import fr.acinq.eclair.wire.protocol.CommonCodecs._
|
||||
import fr.acinq.eclair.wire.protocol.OfferTypes._
|
||||
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequestChain, InvoiceRequestPayerNote, InvoiceRequestQuantity, _}
|
||||
import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tmillisatoshi, tu32, tu64overflow}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, TimestampSecond, UInt64}
|
||||
import fr.acinq.eclair.{TimestampSecond, UInt64}
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
|
||||
object OfferCodecs {
|
||||
private val chains: Codec[Chains] = tlvField(list(bytes32).xmap[Seq[ByteVector32]](_.toSeq, _.toList).as[Chains])
|
||||
private val offerChains: Codec[OfferChains] = tlvField(list(bytes32).xmap[Seq[ByteVector32]](_.toSeq, _.toList))
|
||||
|
||||
private val currency: Codec[Currency] = tlvField(utf8.as[Currency])
|
||||
private val offerMetadata: Codec[OfferMetadata] = tlvField(bytes)
|
||||
|
||||
private val amount: Codec[Amount] = tlvField(tmillisatoshi.as[Amount])
|
||||
private val offerCurrency: Codec[OfferCurrency] = tlvField(utf8)
|
||||
|
||||
private val description: Codec[Description] = tlvField(utf8.as[Description])
|
||||
private val offerAmount: Codec[OfferAmount] = tlvField(tmillisatoshi)
|
||||
|
||||
private val features: Codec[FeaturesTlv] = tlvField(bytes.xmap[Features[Feature]](Features(_), _.toByteVector).as[FeaturesTlv])
|
||||
private val offerDescription: Codec[OfferDescription] = tlvField(utf8)
|
||||
|
||||
private val absoluteExpiry: Codec[AbsoluteExpiry] = tlvField(tu64overflow.as[TimestampSecond].as[AbsoluteExpiry])
|
||||
private val offerFeatures: Codec[OfferFeatures] = tlvField(featuresCodec)
|
||||
|
||||
private val offerAbsoluteExpiry: Codec[OfferAbsoluteExpiry] = tlvField(tu64overflow.as[TimestampSecond])
|
||||
|
||||
private val blindedNodeCodec: Codec[BlindedNode] =
|
||||
(("nodeId" | publicKey) ::
|
||||
("encryptedData" | variableSizeBytes(uint16, bytes))).as[BlindedNode]
|
||||
|
||||
private val blindedNodeCodec: Codec[BlindedNode] = (("nodeId" | publicKey) :: ("encryptedData" | variableSizeBytes(uint16, bytes))).as[BlindedNode]
|
||||
private val blindedNodesCodec: Codec[Seq[BlindedNode]] = listOfN(uint8, blindedNodeCodec).xmap(_.toSeq, _.toList)
|
||||
private val pathCodec: Codec[BlindedRoute] = (("firstNodeId" | publicKey) :: ("blinding" | publicKey) :: ("path" | blindedNodesCodec)).as[BlindedRoute]
|
||||
private val paths: Codec[Paths] = tlvField(list(pathCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList).as[Paths])
|
||||
|
||||
private val issuer: Codec[Issuer] = tlvField(utf8.as[Issuer])
|
||||
private val pathCodec: Codec[BlindedRoute] =
|
||||
(("firstNodeId" | publicKey) ::
|
||||
("blinding" | publicKey) ::
|
||||
("path" | blindedNodesCodec)).as[BlindedRoute]
|
||||
|
||||
private val quantityMin: Codec[QuantityMin] = tlvField(tu64overflow.as[QuantityMin])
|
||||
private val offerPaths: Codec[OfferPaths] = tlvField(list(pathCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList))
|
||||
|
||||
private val quantityMax: Codec[QuantityMax] = tlvField(tu64overflow.as[QuantityMax])
|
||||
private val offerIssuer: Codec[OfferIssuer] = tlvField(utf8)
|
||||
|
||||
private val nodeId: Codec[NodeId] = tlvField(publicKey.as[NodeId])
|
||||
|
||||
private val sendInvoice: Codec[SendInvoice] = tlvField(provide(SendInvoice()))
|
||||
private val offerQuantityMax: Codec[OfferQuantityMax] = tlvField(tu64overflow)
|
||||
|
||||
private val refundFor: Codec[RefundFor] = tlvField(bytes32.as[RefundFor])
|
||||
|
||||
private val signature: Codec[Signature] = tlvField(bytes64.as[Signature])
|
||||
private val offerNodeId: Codec[OfferNodeId] = tlvField(publicKey)
|
||||
|
||||
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
|
||||
.typecase(UInt64(2), offerChains)
|
||||
.typecase(UInt64(4), offerMetadata)
|
||||
.typecase(UInt64(6), offerCurrency)
|
||||
.typecase(UInt64(8), offerAmount)
|
||||
.typecase(UInt64(10), offerDescription)
|
||||
.typecase(UInt64(12), offerFeatures)
|
||||
.typecase(UInt64(14), offerAbsoluteExpiry)
|
||||
.typecase(UInt64(16), offerPaths)
|
||||
.typecase(UInt64(18), offerIssuer)
|
||||
.typecase(UInt64(20), offerQuantityMax)
|
||||
.typecase(UInt64(22), offerNodeId)
|
||||
).complete
|
||||
|
||||
private val chain: Codec[Chain] = tlvField(bytes32.as[Chain])
|
||||
private val invoiceRequestMetadata: Codec[InvoiceRequestMetadata] = tlvField(bytes)
|
||||
|
||||
private val offerId: Codec[OfferId] = tlvField(bytes32.as[OfferId])
|
||||
private val invoiceRequestChain: Codec[InvoiceRequestChain] = tlvField(bytes32)
|
||||
|
||||
private val quantity: Codec[Quantity] = tlvField(tu64overflow.as[Quantity])
|
||||
private val invoiceRequestAmount: Codec[InvoiceRequestAmount] = tlvField(tmillisatoshi)
|
||||
|
||||
private val payerKey: Codec[PayerKey] = tlvField(bytes32.as[PayerKey])
|
||||
private val invoiceRequestFeatures: Codec[InvoiceRequestFeatures] = tlvField(featuresCodec)
|
||||
|
||||
private val payerNote: Codec[PayerNote] = tlvField(utf8.as[PayerNote])
|
||||
private val invoiceRequestQuantity: Codec[InvoiceRequestQuantity] = tlvField(tu64overflow)
|
||||
|
||||
private val payerInfo: Codec[PayerInfo] = tlvField(bytes.as[PayerInfo])
|
||||
private val invoiceRequestPayerId: Codec[InvoiceRequestPayerId] = tlvField(publicKey)
|
||||
|
||||
private val replaceInvoice: Codec[ReplaceInvoice] = tlvField(bytes32.as[ReplaceInvoice])
|
||||
private val invoiceRequestPayerNote: Codec[InvoiceRequestPayerNote] = tlvField(utf8)
|
||||
|
||||
private val signature: Codec[Signature] = tlvField(bytes64)
|
||||
|
||||
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
|
||||
.typecase(UInt64(0), invoiceRequestMetadata)
|
||||
// Offer part that must be copy-pasted from above
|
||||
.typecase(UInt64(2), offerChains)
|
||||
.typecase(UInt64(4), offerMetadata)
|
||||
.typecase(UInt64(6), offerCurrency)
|
||||
.typecase(UInt64(8), offerAmount)
|
||||
.typecase(UInt64(10), offerDescription)
|
||||
.typecase(UInt64(12), offerFeatures)
|
||||
.typecase(UInt64(14), offerAbsoluteExpiry)
|
||||
.typecase(UInt64(16), offerPaths)
|
||||
.typecase(UInt64(18), offerIssuer)
|
||||
.typecase(UInt64(20), offerQuantityMax)
|
||||
.typecase(UInt64(22), offerNodeId)
|
||||
// Invoice request part
|
||||
.typecase(UInt64(80), invoiceRequestChain)
|
||||
.typecase(UInt64(82), invoiceRequestAmount)
|
||||
.typecase(UInt64(84), invoiceRequestFeatures)
|
||||
.typecase(UInt64(86), invoiceRequestQuantity)
|
||||
.typecase(UInt64(88), invoiceRequestPayerId)
|
||||
.typecase(UInt64(89), invoiceRequestPayerNote)
|
||||
.typecase(UInt64(240), signature)
|
||||
).complete
|
||||
|
||||
private val paymentInfo: Codec[PaymentInfo] = (("fee_base_msat" | millisatoshi32) ::
|
||||
("fee_proportional_millionths" | uint32) ::
|
||||
("cltv_expiry_delta" | cltvExpiryDelta) ::
|
||||
("htlc_minimum_msat" | millisatoshi) ::
|
||||
("htlc_maximum_msat" | millisatoshi) ::
|
||||
("features" | lengthPrefixedFeaturesCodec)).as[PaymentInfo]
|
||||
private val invoicePaths: Codec[InvoicePaths] = tlvField(list(pathCodec).xmap[Seq[BlindedRoute]](_.toSeq, _.toList))
|
||||
|
||||
private val paymentPathsInfo: Codec[PaymentPathsInfo] = tlvField(list(paymentInfo).xmap[Seq[PaymentInfo]](_.toSeq, _.toList).as[PaymentPathsInfo])
|
||||
private val paymentInfo: Codec[PaymentInfo] =
|
||||
(("fee_base_msat" | millisatoshi32) ::
|
||||
("fee_proportional_millionths" | uint32) ::
|
||||
("cltv_expiry_delta" | cltvExpiryDelta) ::
|
||||
("htlc_minimum_msat" | millisatoshi) ::
|
||||
("htlc_maximum_msat" | millisatoshi) ::
|
||||
("features" | lengthPrefixedFeaturesCodec)).as[PaymentInfo]
|
||||
|
||||
private val paymentPathsCapacities: Codec[PaymentPathsCapacities] = tlvField(list(millisatoshi).xmap[Seq[MilliSatoshi]](_.toSeq, _.toList).as[PaymentPathsCapacities])
|
||||
private val invoiceBlindedPay: Codec[InvoiceBlindedPay] = tlvField(list(paymentInfo).xmap[Seq[PaymentInfo]](_.toSeq, _.toList))
|
||||
|
||||
private val createdAt: Codec[CreatedAt] = tlvField(tu64overflow.as[TimestampSecond].as[CreatedAt])
|
||||
private val invoiceCreatedAt: Codec[InvoiceCreatedAt] = tlvField(tu64overflow.as[TimestampSecond])
|
||||
|
||||
private val paymentHash: Codec[PaymentHash] = tlvField(bytes32.as[PaymentHash])
|
||||
private val invoiceRelativeExpiry: Codec[InvoiceRelativeExpiry] = tlvField(tu32)
|
||||
|
||||
private val relativeExpiry: Codec[RelativeExpiry] = tlvField(tu32.as[RelativeExpiry])
|
||||
private val invoicePaymentHash: Codec[InvoicePaymentHash] = tlvField(bytes32)
|
||||
|
||||
private val cltv: Codec[Cltv] = tlvField(uint16.as[CltvExpiryDelta].as[Cltv])
|
||||
private val invoiceAmount: Codec[InvoiceAmount] = tlvField(tmillisatoshi)
|
||||
|
||||
private val fallbackAddress: Codec[FallbackAddress] = variableSizeBytesLong(varintoverflow, ("version" | byte) :: ("address" | variableSizeBytes(uint16, bytes))).as[FallbackAddress]
|
||||
private val fallbackAddress: Codec[FallbackAddress] = (("version" | byte) :: ("address" | variableSizeBytes(uint16, bytes))).as[FallbackAddress]
|
||||
|
||||
private val fallbacks: Codec[Fallbacks] = tlvField(list(fallbackAddress).xmap[Seq[FallbackAddress]](_.toSeq, _.toList).as[Fallbacks])
|
||||
private val invoiceFallbacks: Codec[InvoiceFallbacks] = tlvField(list(fallbackAddress).xmap[Seq[FallbackAddress]](_.toSeq, _.toList))
|
||||
|
||||
private val refundSignature: Codec[RefundSignature] = tlvField(bytes64.as[RefundSignature])
|
||||
private val invoiceFeatures: Codec[InvoiceFeatures] = tlvField(featuresCodec)
|
||||
|
||||
private val invoiceNodeId: Codec[InvoiceNodeId] = tlvField(publicKey)
|
||||
|
||||
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), paymentPathsCapacities)
|
||||
.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)
|
||||
// Invoice request part that must be copy-pasted from above
|
||||
.typecase(UInt64(0), invoiceRequestMetadata)
|
||||
.typecase(UInt64(2), offerChains)
|
||||
.typecase(UInt64(4), offerMetadata)
|
||||
.typecase(UInt64(6), offerCurrency)
|
||||
.typecase(UInt64(8), offerAmount)
|
||||
.typecase(UInt64(10), offerDescription)
|
||||
.typecase(UInt64(12), offerFeatures)
|
||||
.typecase(UInt64(14), offerAbsoluteExpiry)
|
||||
.typecase(UInt64(16), offerPaths)
|
||||
.typecase(UInt64(18), offerIssuer)
|
||||
.typecase(UInt64(20), offerQuantityMax)
|
||||
.typecase(UInt64(22), offerNodeId)
|
||||
.typecase(UInt64(80), invoiceRequestChain)
|
||||
.typecase(UInt64(82), invoiceRequestAmount)
|
||||
.typecase(UInt64(84), invoiceRequestFeatures)
|
||||
.typecase(UInt64(86), invoiceRequestQuantity)
|
||||
.typecase(UInt64(88), invoiceRequestPayerId)
|
||||
.typecase(UInt64(89), invoiceRequestPayerNote)
|
||||
// Invoice part
|
||||
.typecase(UInt64(160), invoicePaths)
|
||||
.typecase(UInt64(162), invoiceBlindedPay)
|
||||
.typecase(UInt64(164), invoiceCreatedAt)
|
||||
.typecase(UInt64(166), invoiceRelativeExpiry)
|
||||
.typecase(UInt64(168), invoicePaymentHash)
|
||||
.typecase(UInt64(170), invoiceAmount)
|
||||
.typecase(UInt64(172), invoiceFallbacks)
|
||||
.typecase(UInt64(174), invoiceFeatures)
|
||||
.typecase(UInt64(176), invoiceNodeId)
|
||||
.typecase(UInt64(240), signature)
|
||||
).complete
|
||||
|
||||
|
@ -157,4 +181,8 @@ object OfferCodecs {
|
|||
.typecase(UInt64(5), tlvField(utf8.as[Error]))
|
||||
).complete
|
||||
|
||||
val invoiceRequestCodec: Codec[OnionMessagePayloadTlv.InvoiceRequest] = tlvField(invoiceRequestTlvCodec)
|
||||
val invoiceCodec: Codec[OnionMessagePayloadTlv.Invoice] = tlvField(invoiceTlvCodec)
|
||||
val invoiceErrorCodec: Codec[OnionMessagePayloadTlv.InvoiceError] = tlvField(invoiceErrorTlvCodec)
|
||||
|
||||
}
|
||||
|
|
|
@ -20,9 +20,10 @@ 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.wire.protocol.OnionRoutingCodecs.{InvalidTlvPayload, MissingRequiredTlv}
|
||||
import fr.acinq.eclair.wire.protocol.CommonCodecs.varint
|
||||
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, InvalidTlvPayload, MissingRequiredTlv}
|
||||
import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, InvoiceFeature, MilliSatoshi, TimestampSecond, UInt64, nodeFee}
|
||||
import fr.acinq.eclair.{Bolt12Feature, CltvExpiryDelta, Feature, Features, MilliSatoshi, TimestampSecond, UInt64, nodeFee, randomBytes32}
|
||||
import fr.acinq.secp256k1.Secp256k1JvmKt
|
||||
import scodec.Codec
|
||||
import scodec.bits.ByteVector
|
||||
|
@ -38,27 +39,113 @@ object OfferTypes {
|
|||
|
||||
sealed trait Bolt12Tlv extends Tlv
|
||||
|
||||
sealed trait OfferTlv extends Bolt12Tlv
|
||||
|
||||
sealed trait InvoiceRequestTlv extends Bolt12Tlv
|
||||
|
||||
sealed trait InvoiceTlv extends Bolt12Tlv
|
||||
|
||||
sealed trait InvoiceRequestTlv extends InvoiceTlv
|
||||
|
||||
sealed trait OfferTlv extends InvoiceRequestTlv
|
||||
|
||||
sealed trait InvoiceErrorTlv extends Bolt12Tlv
|
||||
|
||||
case class Chains(chains: Seq[ByteVector32]) extends OfferTlv
|
||||
/**
|
||||
* Chains for which the offer is valid. If empty, bitcoin mainnet is implied.
|
||||
*/
|
||||
case class OfferChains(chains: Seq[ByteVector32]) extends OfferTlv
|
||||
|
||||
case class Currency(iso4217: String) extends OfferTlv
|
||||
/**
|
||||
* Data from the offer creator to themselves, for instance a signature that authenticates the offer so that they don't need to store the offer.
|
||||
*/
|
||||
case class OfferMetadata(data: ByteVector) extends OfferTlv
|
||||
|
||||
case class Amount(amount: MilliSatoshi) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv
|
||||
/**
|
||||
* Three-letter code of the currency the offer is denominated in. If empty, bitcoin is implied.
|
||||
*/
|
||||
case class OfferCurrency(iso4217: String) extends OfferTlv
|
||||
|
||||
case class Description(description: String) extends OfferTlv with InvoiceTlv
|
||||
/**
|
||||
* Amount to pay per item. As we only support bitcoin, the amount is in msat.
|
||||
*/
|
||||
case class OfferAmount(amount: MilliSatoshi) extends OfferTlv
|
||||
|
||||
case class FeaturesTlv(features: Features[Feature]) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv
|
||||
/**
|
||||
* Description of the purpose of the payment.
|
||||
*/
|
||||
case class OfferDescription(description: String) extends OfferTlv
|
||||
|
||||
case class AbsoluteExpiry(absoluteExpiry: TimestampSecond) extends OfferTlv
|
||||
/**
|
||||
* Features supported to pay the offer.
|
||||
*/
|
||||
case class OfferFeatures(features: Features[Feature]) extends OfferTlv
|
||||
|
||||
case class Paths(paths: Seq[BlindedRoute]) extends OfferTlv with InvoiceTlv
|
||||
/**
|
||||
* Time after which the offer is no longer valid.
|
||||
*/
|
||||
case class OfferAbsoluteExpiry(absoluteExpiry: TimestampSecond) extends OfferTlv
|
||||
|
||||
/**
|
||||
* Paths that can be used to retrieve an invoice.
|
||||
*/
|
||||
case class OfferPaths(paths: Seq[BlindedRoute]) extends OfferTlv
|
||||
|
||||
/**
|
||||
* Name of the offer creator.
|
||||
*/
|
||||
case class OfferIssuer(issuer: String) extends OfferTlv
|
||||
|
||||
/**
|
||||
* If present, the item described in the offer can be purchased multiple times with a single payment.
|
||||
* If max = 0, there is no limit on the quantity that can be purchased in a single payment.
|
||||
* If max > 1, it corresponds to the maximum number of items that be purchased in a single payment.
|
||||
*/
|
||||
case class OfferQuantityMax(max: Long) extends OfferTlv
|
||||
|
||||
/**
|
||||
* Public key of the offer creator.
|
||||
* If `OfferPaths` is present, they must be used to retrieve an invoice even if this public key corresponds to a node id in the public network.
|
||||
* If `OfferPaths` is not present, this public key must correspond to a node id in the public network that needs to be contacted to retrieve an invoice.
|
||||
*/
|
||||
case class OfferNodeId(publicKey: PublicKey) extends OfferTlv
|
||||
|
||||
/**
|
||||
* Random data to provide enough entropy so that some fields of the invoice request / invoice can be revealed without revealing the others.
|
||||
*/
|
||||
case class InvoiceRequestMetadata(data: ByteVector) extends InvoiceRequestTlv
|
||||
|
||||
/**
|
||||
* If `OfferChains` is present, this specifies which chain is going to be used to pay.
|
||||
*/
|
||||
case class InvoiceRequestChain(hash: ByteVector32) extends InvoiceRequestTlv
|
||||
|
||||
/**
|
||||
* Amount that the sender is going to send.
|
||||
*/
|
||||
case class InvoiceRequestAmount(amount: MilliSatoshi) extends InvoiceRequestTlv
|
||||
|
||||
/**
|
||||
* Features supported by the sender to pay the offer.
|
||||
*/
|
||||
case class InvoiceRequestFeatures(features: Features[Feature]) extends InvoiceRequestTlv
|
||||
|
||||
/**
|
||||
* Number of items to purchase. Only use if the offer supports purchasing multiple items at once.
|
||||
*/
|
||||
case class InvoiceRequestQuantity(quantity: Long) extends InvoiceRequestTlv
|
||||
|
||||
/**
|
||||
* A public key for which the sender know the corresponding private key.
|
||||
* This can be used to prove that you are the sender.
|
||||
*/
|
||||
case class InvoiceRequestPayerId(publicKey: PublicKey) extends InvoiceRequestTlv
|
||||
|
||||
/**
|
||||
* A message from the sender.
|
||||
*/
|
||||
case class InvoiceRequestPayerNote(note: String) extends InvoiceRequestTlv
|
||||
|
||||
/**
|
||||
* Payment paths to send the payment to.
|
||||
*/
|
||||
case class InvoicePaths(paths: Seq[BlindedRoute]) extends InvoiceTlv
|
||||
|
||||
case class PaymentInfo(feeBase: MilliSatoshi,
|
||||
feeProportionalMillionths: Long,
|
||||
|
@ -69,55 +156,61 @@ object OfferTypes {
|
|||
def fee(amount: MilliSatoshi): MilliSatoshi = nodeFee(feeBase, feeProportionalMillionths, amount)
|
||||
}
|
||||
|
||||
case class PaymentPathsInfo(paymentInfo: Seq[PaymentInfo]) extends InvoiceTlv
|
||||
/**
|
||||
* Costs and parameters of the paths in `InvoicePaths`.
|
||||
*/
|
||||
case class InvoiceBlindedPay(paymentInfo: Seq[PaymentInfo]) extends InvoiceTlv
|
||||
|
||||
case class PaymentPathsCapacities(capacities: Seq[MilliSatoshi]) extends InvoiceTlv
|
||||
/**
|
||||
* Time at which the invoice was created.
|
||||
*/
|
||||
case class InvoiceCreatedAt(timestamp: TimestampSecond) extends InvoiceTlv
|
||||
|
||||
case class Issuer(issuer: String) extends OfferTlv with InvoiceTlv
|
||||
/**
|
||||
* Duration after which the invoice can no longer be paid.
|
||||
*/
|
||||
case class InvoiceRelativeExpiry(seconds: Long) extends InvoiceTlv
|
||||
|
||||
case class QuantityMin(min: Long) extends OfferTlv
|
||||
/**
|
||||
* Hash whose preimage will be released in exchange for the payment.
|
||||
*/
|
||||
case class InvoicePaymentHash(hash: ByteVector32) extends InvoiceTlv
|
||||
|
||||
case class QuantityMax(max: Long) extends OfferTlv
|
||||
|
||||
case class NodeId(publicKey: PublicKey) extends OfferTlv with InvoiceTlv
|
||||
|
||||
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
|
||||
/**
|
||||
* Amount to pay. Must be the same as `InvoiceRequestAmount` if it was present.
|
||||
*/
|
||||
case class InvoiceAmount(amount: MilliSatoshi) extends InvoiceTlv
|
||||
|
||||
case class FallbackAddress(version: Byte, value: ByteVector)
|
||||
|
||||
case class Fallbacks(addresses: Seq[FallbackAddress]) extends InvoiceTlv
|
||||
/**
|
||||
* Onchain addresses to use to pay the invoice in case the lightning payment fails.
|
||||
*/
|
||||
case class InvoiceFallbacks(addresses: Seq[FallbackAddress]) extends InvoiceTlv
|
||||
|
||||
case class RefundSignature(signature: ByteVector64) extends InvoiceTlv
|
||||
/**
|
||||
* Features supported to pay the invoice.
|
||||
*/
|
||||
case class InvoiceFeatures(features: Features[Feature]) extends InvoiceTlv
|
||||
|
||||
/**
|
||||
* Public key of the invoice recipient.
|
||||
*/
|
||||
case class InvoiceNodeId(nodeId: PublicKey) extends InvoiceTlv
|
||||
|
||||
/**
|
||||
* Signature from the sender when used in an invoice request.
|
||||
* Signature from the recipient when used in an invoice.
|
||||
*/
|
||||
case class Signature(signature: ByteVector64) extends InvoiceRequestTlv with InvoiceTlv
|
||||
|
||||
def filterOfferFields(tlvs: TlvStream[InvoiceRequestTlv]): TlvStream[OfferTlv] =
|
||||
// Offer TLVs are in the range (0, 80).
|
||||
TlvStream[OfferTlv](tlvs.records.collect { case tlv: OfferTlv => tlv }, tlvs.unknown.filter(_.tag < UInt64(80)))
|
||||
|
||||
def filterInvoiceRequestFields(tlvs: TlvStream[InvoiceTlv]): TlvStream[InvoiceRequestTlv] =
|
||||
// Invoice request TLVs are in the range [0, 160): invoice request metadata (tag 0), offer TLVs, and additional invoice request TLVs in the range [80, 160).
|
||||
TlvStream[InvoiceRequestTlv](tlvs.records.collect { case tlv: InvoiceRequestTlv => tlv }, tlvs.unknown.filter(_.tag < UInt64(160)))
|
||||
|
||||
case class ErroneousField(tag: Long) extends InvoiceErrorTlv
|
||||
|
||||
|
@ -126,36 +219,22 @@ object OfferTypes {
|
|||
case class Error(message: String) extends InvoiceErrorTlv
|
||||
|
||||
case class Offer(records: TlvStream[OfferTlv]) {
|
||||
val offerId: ByteVector32 = rootHash(removeSignature(records), OfferCodecs.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 chains: Seq[ByteVector32] = records.get[OfferChains].map(_.chains).getOrElse(Seq(Block.LivenetGenesisBlock.hash))
|
||||
val metadata: Option[ByteVector] = records.get[OfferMetadata].map(_.data)
|
||||
val currency: Option[String] = records.get[OfferCurrency].map(_.iso4217)
|
||||
val amount: Option[MilliSatoshi] = currency match {
|
||||
case Some(_) => None // TODO: add exchange rates
|
||||
case None => records.get[Amount].map(_.amount)
|
||||
case None => records.get[OfferAmount].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 nodeId: PublicKey = records.get[NodeId].map(_.publicKey).getOrElse(records.get[Paths].get.paths.head.blindedNodes.last.blindedPublicKey)
|
||||
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: Either[Seq[BlindedRoute], PublicKey] = records.get[Paths].map(_.paths).map(Left(_)).getOrElse(Right(records.get[NodeId].get.publicKey))
|
||||
val description: String = records.get[OfferDescription].get.description
|
||||
val features: Features[Bolt12Feature] = records.get[OfferFeatures].map(_.features.bolt12Features()).getOrElse(Features.empty)
|
||||
val expiry: Option[TimestampSecond] = records.get[OfferAbsoluteExpiry].map(_.absoluteExpiry)
|
||||
private val paths: Option[Seq[BlindedRoute]] = records.get[OfferPaths].map(_.paths)
|
||||
val issuer: Option[String] = records.get[OfferIssuer].map(_.issuer)
|
||||
val quantityMax: Option[Long] = records.get[OfferQuantityMax].map(_.max).map { q => if (q == 0) Long.MaxValue else q }
|
||||
val nodeId: PublicKey = records.get[OfferNodeId].map(_.publicKey).get
|
||||
|
||||
def sign(key: PrivateKey): Offer = {
|
||||
val sig = signSchnorr(Offer.signatureTag, rootHash(records, OfferCodecs.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), OfferCodecs.offerTlvCodec), sig, xOnlyPublicKey(nodeId))
|
||||
case None => false
|
||||
}
|
||||
}
|
||||
val contactInfo: Either[Seq[BlindedRoute], PublicKey] = paths.map(Left(_)).getOrElse(Right(nodeId))
|
||||
|
||||
def encode(): String = {
|
||||
val data = OfferCodecs.offerTlvCodec.encode(records).require.bytes
|
||||
|
@ -167,7 +246,6 @@ object OfferTypes {
|
|||
|
||||
object Offer {
|
||||
val hrp = "lno"
|
||||
val signatureTag: String = "lightning" + "offer" + "signature"
|
||||
|
||||
/**
|
||||
* @param amount_opt amount if it can be determined at offer creation time.
|
||||
|
@ -176,20 +254,21 @@ object OfferTypes {
|
|||
* @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 = {
|
||||
def apply(amount_opt: Option[MilliSatoshi], description: String, nodeId: PublicKey, features: Features[Bolt12Feature], 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,
|
||||
if (chain != Block.LivenetGenesisBlock.hash) Some(OfferChains(Seq(chain))) else None,
|
||||
amount_opt.map(OfferAmount),
|
||||
Some(OfferDescription(description)),
|
||||
if (!features.isEmpty) Some(OfferFeatures(features.unscoped())) else None,
|
||||
Some(OfferNodeId(nodeId)),
|
||||
).flatten
|
||||
Offer(TlvStream(tlvs))
|
||||
}
|
||||
|
||||
def validate(records: TlvStream[OfferTlv]): Either[InvalidTlvPayload, Offer] = {
|
||||
if (records.get[Description].isEmpty) return Left(MissingRequiredTlv(UInt64(10)))
|
||||
if (records.get[NodeId].isEmpty && records.get[Paths].forall(_.paths.isEmpty)) return Left(MissingRequiredTlv(UInt64(30)))
|
||||
if (records.get[OfferDescription].isEmpty) return Left(MissingRequiredTlv(UInt64(10)))
|
||||
if (records.get[OfferNodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(22)))
|
||||
if (records.unknown.exists(_.tag >= UInt64(80))) return Left(ForbiddenTlv(records.unknown.find(_.tag >= UInt64(80)).get.tag))
|
||||
Right(Offer(records))
|
||||
}
|
||||
|
||||
|
@ -209,37 +288,36 @@ object OfferTypes {
|
|||
}
|
||||
|
||||
case class InvoiceRequest(records: TlvStream[InvoiceRequestTlv]) {
|
||||
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
|
||||
val offer: Offer = Offer.validate(filterOfferFields(records)).toOption.get
|
||||
|
||||
def isValidFor(offer: Offer): Boolean = {
|
||||
val metadata: ByteVector = records.get[InvoiceRequestMetadata].get.data
|
||||
val chain: ByteVector32 = records.get[InvoiceRequestChain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash)
|
||||
val amount: Option[MilliSatoshi] = records.get[InvoiceRequestAmount].map(_.amount)
|
||||
val features: Features[Bolt12Feature] = records.get[InvoiceRequestFeatures].map(_.features.bolt12Features()).getOrElse(Features.empty)
|
||||
val quantity_opt: Option[Long] = records.get[InvoiceRequestQuantity].map(_.quantity)
|
||||
val quantity: Long = quantity_opt.getOrElse(1)
|
||||
val payerId: PublicKey = records.get[InvoiceRequestPayerId].get.publicKey
|
||||
val payerNote: Option[String] = records.get[InvoiceRequestPayerNote].map(_.note)
|
||||
private val signature: ByteVector64 = records.get[Signature].get.signature
|
||||
|
||||
def isValidFor(otherOffer: 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 == otherOffer &&
|
||||
amountOk &&
|
||||
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) &&
|
||||
quantity_opt.forall(_ => offer.quantityMax.nonEmpty) &&
|
||||
Features.areCompatible(offer.features, features) &&
|
||||
checkSignature()
|
||||
}
|
||||
|
||||
def checkSignature(): Boolean = {
|
||||
verifySchnorr(InvoiceRequest.signatureTag, rootHash(removeSignature(records), OfferCodecs.invoiceRequestTlvCodec), payerSignature, payerKey)
|
||||
verifySchnorr(InvoiceRequest.signatureTag, rootHash(removeSignature(records), OfferCodecs.invoiceRequestTlvCodec), signature, payerId)
|
||||
}
|
||||
|
||||
def encode(): String = {
|
||||
|
@ -248,11 +326,13 @@ object OfferTypes {
|
|||
}
|
||||
|
||||
override def toString: String = encode()
|
||||
|
||||
def unsigned: TlvStream[InvoiceRequestTlv] = removeSignature(records)
|
||||
}
|
||||
|
||||
object InvoiceRequest {
|
||||
val hrp = "lnr"
|
||||
val signatureTag: String = "lightning" + "invoice_request" + "payer_signature"
|
||||
val signatureTag: ByteVector = ByteVector(("lightning" + "invoice_request" + "signature").getBytes)
|
||||
|
||||
/**
|
||||
* Create a request to fetch an invoice for a given offer.
|
||||
|
@ -264,25 +344,29 @@ object OfferTypes {
|
|||
* @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 = {
|
||||
def apply(offer: Offer, amount: MilliSatoshi, quantity: Long, features: Features[Bolt12Feature], 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)),
|
||||
if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None,
|
||||
).flatten
|
||||
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), OfferCodecs.invoiceRequestTlvCodec), payerKey)
|
||||
InvoiceRequest(TlvStream(tlvs :+ Signature(signature)))
|
||||
require(quantity == 1 || offer.quantityMax.nonEmpty)
|
||||
val tlvs: Seq[InvoiceRequestTlv] = InvoiceRequestMetadata(randomBytes32()) +: (offer.records.records.toSeq ++ Seq(
|
||||
Some(InvoiceRequestChain(chain)),
|
||||
Some(InvoiceRequestAmount(amount)),
|
||||
if (offer.quantityMax.nonEmpty) Some(InvoiceRequestQuantity(quantity)) else None,
|
||||
if (!features.isEmpty) Some(InvoiceRequestFeatures(features.unscoped())) else None,
|
||||
Some(InvoiceRequestPayerId(payerKey.publicKey)),
|
||||
).flatten)
|
||||
val signature = signSchnorr(signatureTag, rootHash(TlvStream(tlvs, offer.records.unknown), OfferCodecs.invoiceRequestTlvCodec), payerKey)
|
||||
InvoiceRequest(TlvStream(tlvs :+ Signature(signature), offer.records.unknown))
|
||||
}
|
||||
|
||||
def validate(records: TlvStream[InvoiceRequestTlv]): Either[InvalidTlvPayload, InvoiceRequest] = {
|
||||
if (records.get[OfferId].isEmpty) return Left(MissingRequiredTlv(UInt64(4)))
|
||||
if (records.get[PayerKey].isEmpty) return Left(MissingRequiredTlv(UInt64(38)))
|
||||
Offer.validate(filterOfferFields(records)).fold(
|
||||
invalidTlvPayload => return Left(invalidTlvPayload),
|
||||
_ -> ()
|
||||
)
|
||||
if (records.get[InvoiceRequestMetadata].isEmpty) return Left(MissingRequiredTlv(UInt64(0)))
|
||||
if (records.get[InvoiceRequestPayerId].isEmpty) return Left(MissingRequiredTlv(UInt64(88)))
|
||||
if (records.get[Signature].isEmpty) return Left(MissingRequiredTlv(UInt64(240)))
|
||||
if (records.unknown.exists(_.tag >= UInt64(160))) return Left(ForbiddenTlv(records.unknown.find(_.tag >= UInt64(160)).get.tag))
|
||||
Right(InvoiceRequest(records))
|
||||
}
|
||||
|
||||
|
@ -318,7 +402,8 @@ object OfferTypes {
|
|||
// 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
|
||||
val firstTlv = genericTlvs.minBy(_.tag)
|
||||
val nonceKey = ByteVector("LnNonce".getBytes) ++ genericTlv.encode(firstTlv).require.bytes
|
||||
|
||||
def previousPowerOfTwo(n: Int): Int = {
|
||||
var p = 1
|
||||
|
@ -331,7 +416,8 @@ object OfferTypes {
|
|||
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))
|
||||
val tlvType = varint.encode(genericTlvs(i).tag).require.bytes
|
||||
(hash(ByteVector("LnLeaf".getBytes), tlv), hash(nonceKey, tlvType))
|
||||
} else {
|
||||
val k = i + previousPowerOfTwo(j - i)
|
||||
(merkleTree(i, k), merkleTree(k, j))
|
||||
|
@ -346,31 +432,23 @@ object OfferTypes {
|
|||
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 = {
|
||||
def signSchnorr(tag: ByteVector, 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 = {
|
||||
def verifySchnorr(tag: ByteVector, msg: ByteVector32, signature: ByteVector64, publicKey: PublicKey): Boolean = {
|
||||
val h = hash(tag, msg)
|
||||
Secp256k1JvmKt.getSecpk256k1.verifySchnorr(signature.toArray, h.toArray, publicKey.toArray)
|
||||
val xonlyPublicKey = publicKey.value.drop(1) // Schnorr signature only use 32 bytes keys.
|
||||
Secp256k1JvmKt.getSecpk256k1.verifySchnorr(signature.toArray, h.toArray, xonlyPublicKey.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)
|
||||
|
|
|
@ -471,7 +471,7 @@ class SphinxSpec extends AnyFunSuite {
|
|||
// The sender includes the correct encrypted recipient data in each blinded node's payload.
|
||||
TlvStream[OnionPaymentPayloadTlv](OnionPaymentPayloadTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(1))),
|
||||
TlvStream[OnionPaymentPayloadTlv](OnionPaymentPayloadTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(2))),
|
||||
TlvStream[OnionPaymentPayloadTlv](OnionPaymentPayloadTlv.AmountToForward(100_000 msat), OnionPaymentPayloadTlv.TotalAmount(150_000 msat), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(749000)), OnionPaymentPayloadTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(3))),
|
||||
TlvStream[OnionPaymentPayloadTlv](OnionPaymentPayloadTlv.AmountToForward(100_000 msat), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(749000)), OnionPaymentPayloadTlv.TotalAmount(150_000 msat), OnionPaymentPayloadTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(3))),
|
||||
).map(tlvs => PaymentOnionCodecs.perHopPayloadCodec.encode(tlvs).require.bytes)
|
||||
assert(payloads == Seq(
|
||||
hex"14020301ae2d04030b6e5e0608000000000000000a",
|
||||
|
|
|
@ -28,7 +28,7 @@ import fr.acinq.eclair.payment._
|
|||
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, NodeHop}
|
||||
import fr.acinq.eclair.wire.protocol.OfferTypes._
|
||||
import fr.acinq.eclair.wire.protocol.{ChannelUpdate, TlvStream, UnknownNextPeer}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, Paginated, ShortChannelId, TimestampMilli, TimestampMilliLong, TimestampSecond, TimestampSecondLong, randomBytes32, randomBytes64, randomKey}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, Paginated, ShortChannelId, TimestampMilli, TimestampMilliLong, TimestampSecond, TimestampSecondLong, randomBytes, randomBytes32, randomBytes64, randomKey}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.bits.HexStringSyntax
|
||||
|
||||
|
@ -516,21 +516,21 @@ class PaymentsDbSpec extends AnyFunSuite {
|
|||
|
||||
val expiredInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = 1 unixsec)
|
||||
val expiredInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #2"), CltvExpiryDelta(18), timestamp = 2 unixsec, expirySeconds = Some(30))
|
||||
val expiredInvoice3 = Bolt12Invoice(TlvStream(Amount(1729 msat), Description("invoice #3"), Paths(Seq(dummyBlindedPath)), PaymentPathsInfo(dummyPathInfo), NodeId(randomKey().publicKey), CreatedAt(3 unixsec), PaymentHash(randomBytes32()), Signature(ByteVector64.Zeroes)))
|
||||
val expiredInvoice3 = Bolt12Invoice(TlvStream(InvoiceRequestMetadata(randomBytes(5)), OfferDescription("invoice #3"), OfferNodeId(randomKey().publicKey), InvoiceRequestAmount(1729 msat), InvoiceRequestPayerId(randomKey().publicKey), InvoicePaths(Seq(dummyBlindedPath)), InvoiceBlindedPay(dummyPathInfo), InvoiceCreatedAt(3 unixsec), InvoicePaymentHash(randomBytes32()), InvoiceAmount(1729 msat), InvoiceNodeId(randomKey().publicKey), Signature(ByteVector64.Zeroes)))
|
||||
val expiredPayment1 = IncomingStandardPayment(expiredInvoice1, randomBytes32(), PaymentType.Standard, expiredInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired)
|
||||
val expiredPayment2 = IncomingStandardPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired)
|
||||
val expiredPayment3 = IncomingBlindedPayment(expiredInvoice3, randomBytes32(), PaymentType.Blinded, Map(randomKey().publicKey -> hex"2a2a2a2a"), expiredInvoice3.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired)
|
||||
|
||||
val pendingInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #4"), CltvExpiryDelta(18), timestamp = TimestampSecond.now() - 10.seconds)
|
||||
val pendingInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #5"), CltvExpiryDelta(18), expirySeconds = Some(30), timestamp = TimestampSecond.now() - 9.seconds)
|
||||
val pendingInvoice3 = Bolt12Invoice(TlvStream(Amount(1729 msat), Description("invoice #6"), Paths(Seq(dummyBlindedPath)), PaymentPathsInfo(dummyPathInfo), NodeId(randomKey().publicKey), CreatedAt(TimestampSecond.now() - 8.seconds), PaymentHash(randomBytes32()), Signature(ByteVector64.Zeroes)))
|
||||
val pendingInvoice3 = Bolt12Invoice(TlvStream(InvoiceRequestMetadata(randomBytes(5)), OfferDescription("invoice #6"), OfferNodeId(randomKey().publicKey), InvoiceRequestAmount(1729 msat), InvoiceRequestPayerId(randomKey().publicKey), InvoicePaths(Seq(dummyBlindedPath)), InvoiceBlindedPay(dummyPathInfo), InvoiceCreatedAt(TimestampSecond.now() - 8.seconds), InvoicePaymentHash(randomBytes32()), InvoiceAmount(1729 msat), InvoiceNodeId(randomKey().publicKey), Signature(ByteVector64.Zeroes)))
|
||||
val pendingPayment1 = IncomingStandardPayment(pendingInvoice1, randomBytes32(), PaymentType.Standard, pendingInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending)
|
||||
val pendingPayment2 = IncomingStandardPayment(pendingInvoice2, randomBytes32(), PaymentType.SwapIn, pendingInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending)
|
||||
val pendingPayment3 = IncomingBlindedPayment(pendingInvoice3, randomBytes32(), PaymentType.Blinded, Map(randomKey().publicKey -> hex"deaddead"), pendingInvoice3.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending)
|
||||
|
||||
val paidInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #7"), CltvExpiryDelta(18), timestamp = TimestampSecond.now() - 5.seconds)
|
||||
val paidInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #8"), CltvExpiryDelta(18), expirySeconds = Some(60), timestamp = TimestampSecond.now() - 4.seconds)
|
||||
val paidInvoice3 = Bolt12Invoice(TlvStream(Amount(1729 msat), Description("invoice #9"), Paths(Seq(dummyBlindedPath)), PaymentPathsInfo(dummyPathInfo), NodeId(randomKey().publicKey), CreatedAt(TimestampSecond.now() - 3.seconds), PaymentHash(randomBytes32()), Signature(ByteVector64.Zeroes)))
|
||||
val paidInvoice3 = Bolt12Invoice(TlvStream(InvoiceRequestMetadata(randomBytes(5)), OfferDescription("invoice #9"), OfferNodeId(randomKey().publicKey), InvoiceRequestAmount(1729 msat), InvoiceRequestPayerId(randomKey().publicKey), InvoicePaths(Seq(dummyBlindedPath)), InvoiceBlindedPay(dummyPathInfo), InvoiceCreatedAt(TimestampSecond.now() - 3.seconds), InvoicePaymentHash(randomBytes32()), InvoiceAmount(1729 msat), InvoiceNodeId(randomKey().publicKey), Signature(ByteVector64.Zeroes)))
|
||||
val receivedAt1 = TimestampMilli.now() + 1.milli
|
||||
val receivedAt2 = TimestampMilli.now() + 2.milli
|
||||
val receivedAt3 = TimestampMilli.now() + 3.milli
|
||||
|
|
|
@ -188,7 +188,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
// we retrieve a payment hash from D
|
||||
val amountMsat = 4200000.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amountMsat), Left("1 coffee")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
// then we make the actual payment, do not randomize the route to make sure we route through node B
|
||||
val sendReq = SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
@ -229,7 +229,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
// first we retrieve a payment hash from D
|
||||
val amountMsat = 300000000.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amountMsat), Left("1 coffee")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
// then we make the payment (B-C has a smaller capacity than A-B and C-D)
|
||||
val sendReq = SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
@ -259,7 +259,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = 200000000.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amountMsat), Left("1 coffee")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
|
||||
// A send payment of only 1 mBTC
|
||||
val sendReq = SendPaymentToNode(100000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5)
|
||||
|
@ -279,7 +279,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = 200000000.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amountMsat), Left("1 coffee")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
|
||||
// A send payment of 6 mBTC
|
||||
val sendReq = SendPaymentToNode(600000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5)
|
||||
|
@ -299,7 +299,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = 200000000.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amountMsat), Left("1 coffee")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
|
||||
// A send payment of 3 mBTC, more than asked but it should still be accepted
|
||||
val sendReq = SendPaymentToNode(300000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5)
|
||||
|
@ -313,7 +313,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
for (_ <- 0 until 7) {
|
||||
val amountMsat = 1000000000.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amountMsat), Left("1 payment")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
|
||||
val sendReq = SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
@ -327,7 +327,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
// first we retrieve a payment hash from C
|
||||
val amountMsat = 2000.msat
|
||||
sender.send(nodes("C").paymentHandler, ReceiveStandardPayment(Some(amountMsat), Left("Change from coffee")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
|
||||
// the payment is requesting to use a capacity-optimized route which will select node G even though it's a bit more expensive
|
||||
sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams.copy(heuristics = Left(WeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0))))))
|
||||
|
@ -341,7 +341,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
val sender = TestProbe()
|
||||
val amount = 1000000000L.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amount), Left("split the restaurant bill")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
assert(invoice.features.hasFeature(Features.BasicMultiPartPayment))
|
||||
|
||||
sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 5, routeParams = integrationTestRouteParams))
|
||||
|
@ -380,7 +380,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
// (the link C->D has too much capacity on D's side).
|
||||
val amount = 2000000000L.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amount), Left("well that's an expensive restaurant bill")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
assert(invoice.features.hasFeature(Features.BasicMultiPartPayment))
|
||||
|
||||
sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels())
|
||||
|
@ -407,7 +407,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
// This amount is greater than any channel capacity between D and C, so it should be split.
|
||||
val amount = 5100000000L.msat
|
||||
sender.send(nodes("C").paymentHandler, ReceiveStandardPayment(Some(amount), Left("lemme borrow some money")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
assert(invoice.features.hasFeature(Features.BasicMultiPartPayment))
|
||||
|
||||
sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 3, routeParams = integrationTestRouteParams))
|
||||
|
@ -435,7 +435,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
// This amount is greater than the current capacity between D and C.
|
||||
val amount = 10000000000L.msat
|
||||
sender.send(nodes("C").paymentHandler, ReceiveStandardPayment(Some(amount), Left("lemme borrow more money")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
assert(invoice.features.hasFeature(Features.BasicMultiPartPayment))
|
||||
|
||||
sender.send(nodes("D").relayer, Relayer.GetOutgoingChannels())
|
||||
|
@ -462,7 +462,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
val sender = TestProbe()
|
||||
val amount = 4000000000L.msat
|
||||
sender.send(nodes("F").paymentHandler, ReceiveStandardPayment(Some(amount), Left("like trampoline much?")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
assert(invoice.features.hasFeature(Features.BasicMultiPartPayment))
|
||||
assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype))
|
||||
|
||||
|
@ -606,7 +606,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
|
||||
// We put most of the capacity C <-> D on D's side.
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(8000000000L msat), Left("plz send everything")))
|
||||
val pr1 = sender.expectMsgType[Invoice]
|
||||
val pr1 = sender.expectMsgType[Bolt11Invoice]
|
||||
sender.send(nodes("C").paymentInitiator, SendPaymentToNode(8000000000L msat, pr1, maxAttempts = 3, routeParams = integrationTestRouteParams))
|
||||
sender.expectMsgType[UUID]
|
||||
sender.expectMsgType[PaymentSent](max = 30 seconds)
|
||||
|
@ -614,7 +614,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
// Now we try to send more than C's outgoing capacity to D.
|
||||
val amount = 2000000000L.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amount), Left("I iz Satoshi")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
assert(invoice.features.hasFeature(Features.BasicMultiPartPayment))
|
||||
assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype))
|
||||
|
||||
|
@ -635,7 +635,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
val sender = TestProbe()
|
||||
val amount = 2000000000L.msat // B can forward to C, but C doesn't have that much outgoing capacity to D
|
||||
sender.send(nodes("D").paymentHandler, ReceiveStandardPayment(Some(amount), Left("I iz not Satoshi")))
|
||||
val invoice = sender.expectMsgType[Invoice]
|
||||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
assert(invoice.features.hasFeature(Features.BasicMultiPartPayment))
|
||||
assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype))
|
||||
|
||||
|
@ -688,17 +688,16 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
val recipientKey = randomKey()
|
||||
val amount = 50_000_000 msat
|
||||
val chain = nodes("D").nodeParams.chainHash
|
||||
val offer = Offer(Some(amount), "test offer", recipientKey.publicKey, nodes("D").nodeParams.features.invoiceFeatures(), chain)
|
||||
val invoiceRequest = InvoiceRequest(offer, amount, 1, nodes("A").nodeParams.features.invoiceFeatures(), randomKey(), chain)
|
||||
val offer = Offer(Some(amount), "test offer", recipientKey.publicKey, nodes("D").nodeParams.features.bolt12Features(), chain)
|
||||
val invoiceRequest = InvoiceRequest(offer, amount, 1, nodes("A").nodeParams.features.bolt12Features(), randomKey(), chain)
|
||||
val receivingRoutes = Seq(
|
||||
ReceivingRoute(Seq(nodes("G").nodeParams.nodeId, nodes("C").nodeParams.nodeId, nodes("D").nodeParams.nodeId), CltvExpiryDelta(1000)),
|
||||
ReceivingRoute(Seq(nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId, nodes("D").nodeParams.nodeId), CltvExpiryDelta(1000)),
|
||||
ReceivingRoute(Seq(nodes("E").nodeParams.nodeId, nodes("C").nodeParams.nodeId, nodes("D").nodeParams.nodeId), CltvExpiryDelta(1000)),
|
||||
)
|
||||
sender.send(nodes("D").paymentHandler, ReceiveOfferPayment(recipientKey, offer, invoiceRequest, receivingRoutes, nodes("D").router))
|
||||
sender.send(nodes("D").paymentHandler, ReceiveOfferPayment(recipientKey, invoiceRequest, receivingRoutes, nodes("D").router))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.blindedPaths.length == 3)
|
||||
assert(invoice.blindedPathsInfo.length == 3)
|
||||
assert(invoice.nodeId == recipientKey.publicKey)
|
||||
|
||||
sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 3, routeParams = integrationTestRouteParams))
|
||||
|
@ -718,17 +717,17 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
val sender = TestProbe()
|
||||
val amount = 25_000_000 msat
|
||||
val chain = nodes("C").nodeParams.chainHash
|
||||
val offer = Offer(Some(amount), "test offer", nodes("C").nodeParams.nodeId, nodes("C").nodeParams.features.invoiceFeatures(), chain)
|
||||
val invoiceRequest = InvoiceRequest(offer, amount, 1, nodes("D").nodeParams.features.invoiceFeatures(), randomKey(), chain)
|
||||
val offer = Offer(Some(amount), "test offer", nodes("C").nodeParams.nodeId, nodes("C").nodeParams.features.bolt12Features(), chain)
|
||||
val invoiceRequest = InvoiceRequest(offer, amount, 1, nodes("D").nodeParams.features.bolt12Features(), randomKey(), chain)
|
||||
// C uses a 0-hop blinded route and signs the invoice with its public nodeId.
|
||||
val receivingRoutes = Seq(
|
||||
ReceivingRoute(Seq(nodes("C").nodeParams.nodeId), CltvExpiryDelta(1000)),
|
||||
ReceivingRoute(Seq(nodes("C").nodeParams.nodeId), CltvExpiryDelta(1000)),
|
||||
)
|
||||
sender.send(nodes("C").paymentHandler, ReceiveOfferPayment(nodes("C").nodeParams.privateKey, offer, invoiceRequest, receivingRoutes, nodes("C").router))
|
||||
sender.send(nodes("C").paymentHandler, ReceiveOfferPayment(nodes("C").nodeParams.privateKey, invoiceRequest, receivingRoutes, nodes("C").router))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.blindedPaths.length == 2)
|
||||
assert(invoice.blindedPaths.forall(_.length == 0))
|
||||
assert(invoice.blindedPaths.forall(_.route.length == 0))
|
||||
assert(invoice.nodeId == nodes("C").nodeParams.nodeId)
|
||||
|
||||
sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 3, routeParams = integrationTestRouteParams))
|
||||
|
@ -748,12 +747,12 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
val recipientKey = randomKey()
|
||||
val amount = 50_000_000 msat
|
||||
val chain = nodes("A").nodeParams.chainHash
|
||||
val offer = Offer(Some(amount), "test offer", recipientKey.publicKey, nodes("A").nodeParams.features.invoiceFeatures(), chain)
|
||||
val invoiceRequest = InvoiceRequest(offer, amount, 1, nodes("B").nodeParams.features.invoiceFeatures(), randomKey(), chain)
|
||||
val offer = Offer(Some(amount), "test offer", recipientKey.publicKey, nodes("A").nodeParams.features.bolt12Features(), chain)
|
||||
val invoiceRequest = InvoiceRequest(offer, amount, 1, nodes("B").nodeParams.features.bolt12Features(), randomKey(), chain)
|
||||
val receivingRoutes = Seq(
|
||||
ReceivingRoute(Seq(nodes("A").nodeParams.nodeId), CltvExpiryDelta(1000), Seq(DummyBlindedHop(100 msat, 100, CltvExpiryDelta(48)), DummyBlindedHop(150 msat, 50, CltvExpiryDelta(36))))
|
||||
)
|
||||
sender.send(nodes("A").paymentHandler, ReceiveOfferPayment(recipientKey, offer, invoiceRequest, receivingRoutes, nodes("A").router))
|
||||
sender.send(nodes("A").paymentHandler, ReceiveOfferPayment(recipientKey, invoiceRequest, receivingRoutes, nodes("A").router))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.blindedPaths.length == 1)
|
||||
assert(invoice.nodeId == recipientKey.publicKey)
|
||||
|
|
|
@ -102,9 +102,8 @@ class BlindedPaymentSpec extends FixtureSpec with IntegrationPatience {
|
|||
val offerKey = randomKey()
|
||||
val offer = Offer(None, "test", offerKey.publicKey, Features.empty, recipient.nodeParams.chainHash)
|
||||
val invoiceReq = InvoiceRequest(offer, amount, 1, Features.empty, randomKey(), recipient.nodeParams.chainHash)
|
||||
sender.send(recipient.paymentHandler, MultiPartHandler.ReceiveOfferPayment(offerKey, offer, invoiceReq, routes, recipient.router))
|
||||
sender.send(recipient.paymentHandler, MultiPartHandler.ReceiveOfferPayment(offerKey, invoiceReq, routes, recipient.router))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.nodeId != recipient.nodeParams.nodeId)
|
||||
invoice
|
||||
}
|
||||
|
||||
|
|
|
@ -191,9 +191,9 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers {
|
|||
}
|
||||
|
||||
test("Bolt 12 invoice") {
|
||||
val ref = "lni1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypmpsc7ww3cxguwl27ela95ykset7t8tlvyfy7a200eujcnhczws6zqyrvhqd5s2p4kkjmnfd4skcgr0venx2ussmvpexwyy4tcadvgg89l9aljus6709kx235hhqrk6n8dey98uyuftzdqrshx2mcmnvj7gxa709vhgcrqr7hcdp7l7x9t2au8dj8tjreqyfvuqyqkp4czrrxpn3hdrdqu8k3teynrl4nf977deq2ja53zkefmsr0nyjgqp3dtytq67durd2jjupmnjdtvvlsuw7lsm6tvcyrrqx7pwaunazjuqn2uk9gvpzj0aeagku2h2wv8vcfmfekflxmfmgu0kqqa94fuhuhyn6jxefk7k5rfgejltw7cwa4fhjl67trweukvsw4wqq7ec790vdjar7qwtsnlfrq84fmq02ah49fvnnsj06j5wzgwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqm9crdyqqqrcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4ycsra98j4l2k35fg7qhvapz26js2rh0j5n36pzlt9kaprvl3zd9s29egq33khv3m9gszev88kpfrveu8g5xr8khk6tev8jmpxg3pxfhpcx6f4jtlm4ltwgpwqgqpduzqyec5qspkg0zqq788krw2w2kstvsz3dekms304ykkh395zl6chm34vdu03yuvgwzm0580zu6sp2f07uwa4crgvkgucd8zdpt5vu302nc"
|
||||
val ref = "lni1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh5zsq83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4q2rd3ny0elv9m7mh38xxwe6ypfheeqeqlwgft05r6dhc50gtw0nv2qgrrl9x2qzzqvwukam32mhkdqrvwwcp5l6jcnnnezdq69vz8gdvvgmsqwk3efqf3f6gmf0ul63940awz429rdhhsts86s0r30e5nffwhrqw90xgxf7f60sm7tcclvyqwz7cer5q9223madstdy2p5q6y8qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf2qheqqqqq2gprrgshynfszqyk2sgpvkrnmq53kv7r52rpnmtmd9ukredsnygsnymsurdy6e9la6l4hyz4qgxewqmftqggrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet6lqsx4k5r7rsld3hhe87psyy5cnhhzt4dz838f75734mted7pdsrflpvys23tkafmhctf3musnsaa42h6qjdggyqlhtevutzzpzlnwd8alq"
|
||||
val pr = Invoice.fromString(ref).get
|
||||
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"amount":456001234,"nodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","paymentHash":"2cb0e7b052366787450c33daf6d2f2c3cb6132221326e1c1b49ac97fdd7eb720","description":"minimal offer","features":{"activated":{},"unknown":[]},"blindedPaths":[{"introductionNodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","blindedNodeIds":["02c1ae043198338dda368387b457924c7facd25f79b902a5da4456ca7701be6492","03782eef27d14b809ab962a181149fdcf516e2aea730ecc2769cd93f36d3b471f6"]}],"createdAt":1668002363,"expiresAt":1668009563,"serialized":"lni1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypmpsc7ww3cxguwl27ela95ykset7t8tlvyfy7a200eujcnhczws6zqyrvhqd5s2p4kkjmnfd4skcgr0venx2ussmvpexwyy4tcadvgg89l9aljus6709kx235hhqrk6n8dey98uyuftzdqrshx2mcmnvj7gxa709vhgcrqr7hcdp7l7x9t2au8dj8tjreqyfvuqyqkp4czrrxpn3hdrdqu8k3teynrl4nf977deq2ja53zkefmsr0nyjgqp3dtytq67durd2jjupmnjdtvvlsuw7lsm6tvcyrrqx7pwaunazjuqn2uk9gvpzj0aeagku2h2wv8vcfmfekflxmfmgu0kqqa94fuhuhyn6jxefk7k5rfgejltw7cwa4fhjl67trweukvsw4wqq7ec790vdjar7qwtsnlfrq84fmq02ah49fvnnsj06j5wzgwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqm9crdyqqqrcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4ycsra98j4l2k35fg7qhvapz26js2rh0j5n36pzlt9kaprvl3zd9s29egq33khv3m9gszev88kpfrveu8g5xr8khk6tev8jmpxg3pxfhpcx6f4jtlm4ltwgpwqgqpduzqyec5qspkg0zqq788krw2w2kstvsz3dekms304ykkh395zl6chm34vdu03yuvgwzm0580zu6sp2f07uwa4crgvkgucd8zdpt5vu302nc"}"""
|
||||
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"amount":456001234,"nodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","paymentHash":"2cb0e7b052366787450c33daf6d2f2c3cb6132221326e1c1b49ac97fdd7eb720","description":"minimal offer","features":{"activated":{"var_onion_optin":"mandatory","option_route_blinding":"mandatory"},"unknown":[]},"blindedPaths":[{"introductionNodeId":"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5","blindedNodeIds":["031fca650042031dcb777156ef66806c73b01a7f52c4e73c89a0d15823a1ac6237"]}],"createdAt":1665412681,"expiresAt":1665412981,"serialized":"lni1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh5zsq83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4q2rd3ny0elv9m7mh38xxwe6ypfheeqeqlwgft05r6dhc50gtw0nv2qgrrl9x2qzzqvwukam32mhkdqrvwwcp5l6jcnnnezdq69vz8gdvvgmsqwk3efqf3f6gmf0ul63940awz429rdhhsts86s0r30e5nffwhrqw90xgxf7f60sm7tcclvyqwz7cer5q9223madstdy2p5q6y8qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqf2qheqqqqq2gprrgshynfszqyk2sgpvkrnmq53kv7r52rpnmtmd9ukredsnygsnymsurdy6e9la6l4hyz4qgxewqmftqggrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet6lqsx4k5r7rsld3hhe87psyy5cnhhzt4dz838f75734mted7pdsrflpvys23tkafmhctf3musnsaa42h6qjdggyqlhtevutzzpzlnwd8alq"}"""
|
||||
}
|
||||
|
||||
test("GlobalBalance serializer") {
|
||||
|
|
|
@ -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.Features.{PaymentMetadata, PaymentSecret, _}
|
||||
import fr.acinq.eclair.payment.Bolt11Invoice._
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, payment, randomBytes32}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32}
|
||||
import org.scalatest.TryValues.convertTryToSuccessOrFailure
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.DecodeResult
|
||||
|
@ -632,7 +632,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
|
|||
}
|
||||
|
||||
test("no unknown feature in invoice") {
|
||||
val invoiceFeatures = TestConstants.Alice.nodeParams.features.invoiceFeatures().remove(RouteBlinding)
|
||||
val invoiceFeatures = TestConstants.Alice.nodeParams.features.bolt11Features().remove(RouteBlinding)
|
||||
assert(invoiceFeatures.unknown.nonEmpty)
|
||||
val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = invoiceFeatures)
|
||||
assert(invoice.features == Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory))
|
||||
|
|
|
@ -18,11 +18,11 @@ 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, Crypto}
|
||||
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.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.Bolt12Invoice.{hrp, signatureTag}
|
||||
import fr.acinq.eclair.payment.Bolt12Invoice.hrp
|
||||
import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceRequestTlvCodec, invoiceTlvCodec}
|
||||
import fr.acinq.eclair.wire.protocol.OfferTypes._
|
||||
import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataCodecs.blindedRouteDataCodec
|
||||
|
@ -38,7 +38,7 @@ import scala.util.Success
|
|||
class Bolt12InvoiceSpec extends AnyFunSuite {
|
||||
|
||||
def signInvoiceTlvs(tlvs: TlvStream[InvoiceTlv], key: PrivateKey): TlvStream[InvoiceTlv] = {
|
||||
val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), key)
|
||||
val signature = signSchnorr(Bolt12Invoice.signatureTag, rootHash(tlvs, invoiceTlvCodec), key)
|
||||
tlvs.copy(records = tlvs.records ++ Seq(Signature(signature)))
|
||||
}
|
||||
|
||||
|
@ -58,219 +58,124 @@ class Bolt12InvoiceSpec extends AnyFunSuite {
|
|||
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, CltvExpiryDelta(20), Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
assert(invoice.isValidFor(offer, request))
|
||||
val invoice = Bolt12Invoice(request, randomBytes32(), nodeKey, 300 seconds, Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
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))
|
||||
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"))))
|
||||
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))
|
||||
val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferAmount(amount) => OfferAmount(amount + 100.msat) case x => x }, invoice.records.unknown))
|
||||
assert(!withModifiedAmount.checkSignature())
|
||||
assert(!withModifiedAmount.isValidFor(offer, request))
|
||||
assert(!withModifiedAmount.checkRefundSignature())
|
||||
}
|
||||
|
||||
test("check invoice signature with unknown field from invoice request") {
|
||||
val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32())
|
||||
val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain)
|
||||
val basicRequest = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain)
|
||||
val requestWithUnknownTlv = basicRequest.copy(records = TlvStream(basicRequest.records.records, Seq(GenericTlv(UInt64(87), hex"0404"))))
|
||||
val invoice = Bolt12Invoice(requestWithUnknownTlv, randomBytes32(), nodeKey, 300 seconds, Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
assert(invoice.records.unknown == Seq(GenericTlv(UInt64(87), hex"0404")))
|
||||
assert(invoice.isValidFor(requestWithUnknownTlv))
|
||||
assert(Bolt12Invoice.fromString(invoice.toString).get.toString == invoice.toString)
|
||||
}
|
||||
|
||||
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, CltvExpiryDelta(20), Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
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)), 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)), 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)), nodeKey)
|
||||
assert(withExtendedDescription.isValidFor(offer, request))
|
||||
val invoice = Bolt12Invoice(request, randomBytes32(), nodeKey, 300 seconds, Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
assert(invoice.isValidFor(request))
|
||||
// amount must match the request
|
||||
val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferAmount(_) => OfferAmount(9000 msat) case x => x }.toSeq)), nodeKey)
|
||||
assert(!withOtherAmount.isValidFor(request))
|
||||
// description must match the offer
|
||||
val withOtherDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferDescription(_) => OfferDescription("other description") case x => x }.toSeq)), nodeKey)
|
||||
assert(!withOtherDescription.isValidFor(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)), 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)), nodeKey)
|
||||
assert(!withOtherOfferId.isValidFor(offer, request))
|
||||
val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferNodeId(_) => OfferNodeId(otherNodeKey.publicKey) case x => x }.toSeq)), nodeKey)
|
||||
assert(!withOtherNodeId.isValidFor(request))
|
||||
// issuer must match the offer
|
||||
val withOtherIssuer = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records ++ Seq(Issuer("spongebob")))), nodeKey)
|
||||
assert(!withOtherIssuer.isValidFor(offer, request))
|
||||
val withOtherIssuer = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records ++ Seq(OfferIssuer("spongebob")))), nodeKey)
|
||||
assert(!withOtherIssuer.isValidFor(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)
|
||||
val offer = Offer(Some(15000 msat), "test offer", nodeKey.publicKey, Features.empty, chain)
|
||||
val request = InvoiceRequest(offer, 15000 msat, 1, Features.empty, payerKey, chain)
|
||||
assert(request.quantity_opt.isEmpty) // when paying for a single item, the quantity field must not be present
|
||||
val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional), Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
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)), nodeKey)
|
||||
assert(!withInvalidFeatures.isValidFor(offer, request))
|
||||
val withAmountTooBig = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(20000 msat) case x => x }.toSeq)), nodeKey)
|
||||
assert(!withAmountTooBig.isValidFor(offer, request))
|
||||
val withQuantity = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ Quantity(2))), nodeKey)
|
||||
assert(!withQuantity.isValidFor(offer, request))
|
||||
val withOtherPayerKey = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case PayerKey(_) => PayerKey(randomBytes32()) case x => x }.toSeq)), nodeKey)
|
||||
assert(!withOtherPayerKey.isValidFor(offer, request))
|
||||
val withPayerNote = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerNote("I am Batman"))), nodeKey)
|
||||
assert(!withPayerNote.isValidFor(offer, request))
|
||||
val withPayerInfo = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerInfo(hex"010203040506"))), nodeKey)
|
||||
assert(!withPayerInfo.isValidFor(offer, request))
|
||||
val invoice = Bolt12Invoice(request, randomBytes32(), nodeKey, 300 seconds, Features(BasicMultiPartPayment -> Optional), Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
assert(invoice.isValidFor(request))
|
||||
val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case InvoiceFeatures(_) => InvoiceFeatures(Features(BasicMultiPartPayment -> Mandatory)) case x => x }.toSeq)), nodeKey)
|
||||
assert(!withInvalidFeatures.isValidFor(request))
|
||||
val withAmountTooBig = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case InvoiceRequestAmount(_) => InvoiceRequestAmount(20000 msat) case x => x }.toSeq)), nodeKey)
|
||||
assert(!withAmountTooBig.isValidFor(request))
|
||||
val withQuantity = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ InvoiceRequestQuantity(2))), nodeKey)
|
||||
assert(!withQuantity.isValidFor(request))
|
||||
val withOtherPayerKey = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case InvoiceRequestPayerId(_) => InvoiceRequestPayerId(randomKey().publicKey) case x => x }.toSeq)), nodeKey)
|
||||
assert(!withOtherPayerKey.isValidFor(request))
|
||||
val withPayerNote = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ InvoiceRequestPayerNote("I am Batman"))), nodeKey)
|
||||
assert(!withPayerNote.isValidFor(request))
|
||||
val withOtherMetadata = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case InvoiceRequestMetadata(_) => InvoiceRequestMetadata(hex"ae46c46b86") case x => x }.toSeq)), nodeKey)
|
||||
assert(!withOtherMetadata.isValidFor(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))
|
||||
InvoiceRequestMetadata(hex"010203040506"),
|
||||
OfferDescription("offer description"),
|
||||
OfferNodeId(nodeKey.publicKey),
|
||||
InvoiceRequestAmount(15000 msat),
|
||||
InvoiceRequestPayerId(payerKey.publicKey),
|
||||
InvoiceRequestPayerNote("I am Batman"),
|
||||
OfferFeatures(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, CltvExpiryDelta(20), Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
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)), 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)), nodeKey)
|
||||
assert(!withOtherPayerNote.isValidFor(offer, requestWithPayerDetails))
|
||||
assert(!withOtherPayerNote.isValidFor(offer, request))
|
||||
val withPayerDetails = Bolt12Invoice(requestWithPayerDetails, randomBytes32(), nodeKey, 300 seconds, Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
assert(withPayerDetails.isValidFor(requestWithPayerDetails))
|
||||
assert(!withPayerDetails.isValidFor(request))
|
||||
val withOtherPayerNote = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case InvoiceRequestPayerNote(_) => InvoiceRequestPayerNote("Or am I Bruce Wayne?") case x => x }.toSeq)), nodeKey)
|
||||
assert(!withOtherPayerNote.isValidFor(requestWithPayerDetails))
|
||||
assert(!withOtherPayerNote.isValidFor(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, CltvExpiryDelta(20), Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
val invoice = Bolt12Invoice(request, randomBytes32(), nodeKey, 300 seconds, Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
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 })), nodeKey)
|
||||
assert(invoice.isValidFor(request))
|
||||
val expiredInvoice1 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case InvoiceCreatedAt(_) => InvoiceCreatedAt(0 unixsec) case x => x })), 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)))), nodeKey)
|
||||
assert(!expiredInvoice1.isValidFor(request)) // when an invoice is expired, we mark it as invalid as well
|
||||
val expiredInvoice2 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map {
|
||||
case InvoiceCreatedAt(_) => InvoiceCreatedAt(TimestampSecond.now() - 2000)
|
||||
case InvoiceRelativeExpiry(_) => InvoiceRelativeExpiry(1800)
|
||||
case x => x
|
||||
})), 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 paymentBlindedRoute = createPaymentBlindedRoute(nodeKey.publicKey)
|
||||
val invoiceImplicitBtc = {
|
||||
val tlvs: Seq[InvoiceTlv] = Seq(
|
||||
CreatedAt(TimestampSecond.now()),
|
||||
PaymentHash(Crypto.sha256(randomBytes32())),
|
||||
OfferId(offerBtc.offerId),
|
||||
NodeId(nodeKey.publicKey),
|
||||
Paths(Seq(paymentBlindedRoute.route)),
|
||||
PaymentPathsInfo(Seq(paymentBlindedRoute.paymentInfo)),
|
||||
Amount(amount),
|
||||
Description(offerBtc.description),
|
||||
PayerKey(payerKey.publicKey)
|
||||
)
|
||||
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)))
|
||||
}
|
||||
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),
|
||||
Paths(Seq(paymentBlindedRoute.route)),
|
||||
PaymentPathsInfo(Seq(paymentBlindedRoute.paymentInfo)),
|
||||
Amount(amount),
|
||||
Description(offerBtc.description),
|
||||
PayerKey(payerKey.publicKey)
|
||||
)
|
||||
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)))
|
||||
}
|
||||
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),
|
||||
Paths(Seq(paymentBlindedRoute.route)),
|
||||
PaymentPathsInfo(Seq(paymentBlindedRoute.paymentInfo)),
|
||||
Amount(amount),
|
||||
Description(offerBtc.description),
|
||||
PayerKey(payerKey.publicKey)
|
||||
)
|
||||
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)))
|
||||
}
|
||||
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),
|
||||
Paths(Seq(paymentBlindedRoute.route)),
|
||||
PaymentPathsInfo(Seq(paymentBlindedRoute.paymentInfo)),
|
||||
Amount(amount),
|
||||
Description(offerOtherChains.description),
|
||||
PayerKey(payerKey.publicKey)
|
||||
)
|
||||
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)))
|
||||
}
|
||||
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),
|
||||
Paths(Seq(paymentBlindedRoute.route)),
|
||||
PaymentPathsInfo(Seq(paymentBlindedRoute.paymentInfo)),
|
||||
Amount(amount),
|
||||
Description(offerOtherChains.description),
|
||||
PayerKey(payerKey.publicKey)
|
||||
)
|
||||
val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey)
|
||||
Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)))
|
||||
}
|
||||
assert(!invoiceInvalidOtherChain.isValidFor(offerOtherChains, requestOtherChains))
|
||||
val invoiceMissingChain = signInvoice(Bolt12Invoice(TlvStream(invoiceOtherChains.records.records.filter { case Chain(_) => false case _ => true })), nodeKey)
|
||||
assert(!invoiceMissingChain.isValidFor(offerOtherChains, requestOtherChains))
|
||||
assert(!expiredInvoice2.isValidFor(request)) // when an invoice is expired, we mark it as invalid as well
|
||||
}
|
||||
|
||||
test("decode invalid invoice") {
|
||||
val nodeKey = randomKey()
|
||||
val tlvs = Seq[InvoiceTlv](
|
||||
Amount(765432 msat),
|
||||
Description("minimal invoice"),
|
||||
NodeId(nodeKey.publicKey),
|
||||
Paths(Seq(createPaymentBlindedRoute(randomKey().publicKey).route)),
|
||||
PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 765432 msat, Features.empty))),
|
||||
CreatedAt(TimestampSecond(123456789L)),
|
||||
PaymentHash(randomBytes32()),
|
||||
InvoiceRequestMetadata(hex"012345"),
|
||||
OfferDescription("minimal invoice"),
|
||||
OfferNodeId(nodeKey.publicKey),
|
||||
InvoiceRequestPayerId(randomKey().publicKey),
|
||||
InvoicePaths(Seq(createPaymentBlindedRoute(randomKey().publicKey).route)),
|
||||
InvoiceBlindedPay(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 765432 msat, Features.empty))),
|
||||
InvoiceCreatedAt(TimestampSecond(123456789L)),
|
||||
InvoicePaymentHash(randomBytes32()),
|
||||
InvoiceAmount(1684 msat),
|
||||
InvoiceNodeId(nodeKey.publicKey),
|
||||
)
|
||||
// This minimal invoice is valid.
|
||||
val signed = signInvoiceTlvs(TlvStream(tlvs), nodeKey)
|
||||
|
@ -290,68 +195,63 @@ class Bolt12InvoiceSpec extends AnyFunSuite {
|
|||
|
||||
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 features = Features[Feature](Features.VariableLengthOnion -> FeatureSupport.Mandatory, Features.RouteBlinding -> FeatureSupport.Mandatory)
|
||||
val issuer = "alice"
|
||||
val nodeKey = PrivateKey(hex"998cf8ecab46f949bb960813b79d3317cabf4193452a211795cd8af1b9a25d90")
|
||||
val path = createPaymentBlindedRoute(nodeKey.publicKey, PrivateKey(hex"f0442c17bdd2cefe4a4ede210f163b068bb3fea6113ffacea4f322de7aa9737b"), hex"76030536ba732cdc4e7bb0a883750bab2e88cb3dddd042b1952c44b4849c86bb").route
|
||||
val payInfo = PaymentInfo(2345 msat, 765, CltvExpiryDelta(324), 1000 msat, amount, Features.empty)
|
||||
val path = createPaymentBlindedRoute(nodeKey.publicKey, PrivateKey(hex"f0442c17bdd2cefe4a4ede210f163b068bb3fea6113ffacea4f322de7aa9737b"), hex"76030536ba732cdc4e7bb0a883750bab2e88cb3dddd042b1952c44b4849c86bb").copy(paymentInfo = PaymentInfo(2345 msat, 765, CltvExpiryDelta(324), 1000 msat, amount, Features.empty))
|
||||
val quantity = 57
|
||||
val payerKey = ByteVector32.fromValidHex("8faadd71b1f78b16265e5b061b9d2b88891012dc7ad38626eeaaa2a271615a65")
|
||||
val payerKey = PublicKey(hex"024a8d96f4d13c4219f211b8a8e7b4ab7a898fd1b2e90274ca5a8737a9eda377f8")
|
||||
val payerNote = "I'm Bob"
|
||||
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),
|
||||
Paths(Seq(path)),
|
||||
PaymentPathsInfo(Seq(payInfo)),
|
||||
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)
|
||||
InvoiceRequestMetadata(payerInfo),
|
||||
OfferChains(Seq(chain)),
|
||||
OfferAmount(amount),
|
||||
OfferDescription(description),
|
||||
OfferFeatures(Features.empty),
|
||||
OfferIssuer(issuer),
|
||||
OfferNodeId(nodeKey.publicKey),
|
||||
InvoiceRequestChain(chain),
|
||||
InvoiceRequestAmount(amount),
|
||||
InvoiceRequestQuantity(quantity),
|
||||
InvoiceRequestPayerId(payerKey),
|
||||
InvoiceRequestPayerNote(payerNote),
|
||||
InvoicePaths(Seq(path.route)),
|
||||
InvoiceBlindedPay(Seq(path.paymentInfo)),
|
||||
InvoiceCreatedAt(createdAt),
|
||||
InvoiceRelativeExpiry(relativeExpiry),
|
||||
InvoicePaymentHash(paymentHash),
|
||||
InvoiceAmount(amount),
|
||||
InvoiceFallbacks(fallbacks),
|
||||
InvoiceFeatures(Features.empty),
|
||||
InvoiceNodeId(nodeKey.publicKey),
|
||||
), Seq(GenericTlv(UInt64(121), hex"010203"), GenericTlv(UInt64(313), hex"baba")))
|
||||
val signature = signSchnorr(Bolt12Invoice.signatureTag, rootHash(tlvs, invoiceTlvCodec), nodeKey)
|
||||
val invoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))))
|
||||
assert(invoice.toString == "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsqy9qq0zxw03kpc8tc2vv3kfdne0kntqhq8p70wtdncwq2zngaqp529mmcq5ecw92k3597h7kdndc64mg2xt709acf2gmxnnag5kq9a6wslznscqsyu5p4eckl7m69k0qpcppkpz3lq4chus9szjkgw9w7mgeknz7m7fpqqa02qmqdj08z62mz0jws0gxt45fyq8udel9jg5gd6xlgdrkdt5qywp0jna8fws7jvdur0nayh63fjeey5w8pmqw7s3lcjunzgwqqqqf9yqqqqhaq9zqqqqqqqqqqqlgqqqqqqqqq83yqqqqzszkzmrfvdj3uggrc3nnudswp67znrydjtv7ta56c9cpc0nmjmv7rszs568gqdz3w77zqqfeycsgl2kawxcl0zckye09kpsmn54c3zgsztw845uxymh24g4zw9s45ef8qayjwmfqgfhky2qyv2sqd032ypge282v20ysgq6lpv5nmjwlrs88jeepxsc2upa970snfnfnxff5ztqzpcgzuqsq0vcpgpcyqqzpy02klq9svqqgavadc6y5tmmqzvsv484ku5nw43vumxuflvsrsgr345pnuh6zq6pz2cy8wra8vujs23y5yhd4gwslns3m7qm9023hc8cyq7e6y8ywe85k5ey9twjy026s9akr0hlw8faqkp4cguquhlrw2uwwqe3wtfn3wxv58t8g8pqf0afnw2f6247yqp4k6jgcq9eh8ua7f2kl6qfhqvqsyqlaqyusq")
|
||||
assert(invoice.toString == "lni1qqx2n6mw2fh2ckwdnwylkgqzypp5jl7hlqnf2ugg7j3slkwwcwht57vhyzzwjr4dq84rxzgqqqqqqzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqqfq2ctvd93k293pq0zxw03kpc8tc2vv3kfdne0kntqhq8p70wtdncwq2zngaqp529mmc5pqgdyhl4lcy62hzz855v8annkr46a8n9eqsn5satgpagesjqqqqqq9yqcpufq9vqfetqssyj5djm6dz0zzr8eprw9gu762k75f3lgm96gzwn994peh48k6xalctyr5jfmdyppx7cneqvqsyqaq5qpugee7xc8qa0pf3jxe9k0976dvzuqu8eaedk0pcpg2dr5qx3gh00qzn8pc426xsh6l6ekdhr2hdpge0euhhp9frv6w04zjcqhhf6ru2wrqzqnjsxh8zmlm0gkeuq8qyxcy28uzhzljqkq22epc4mmdrx6vtm0eyyqr4agrvpkfuutftvf7f6paqewk3ysql3h8ukfz3phgmap5we4wsq3c97205a96r6f3hsd705jl29xt8yj3cu8vpm6z8lztjw3pcqqqpy5sqqqzl5q5gqqqqqqqqqqraqqqqqqqqqq7ysqqqzjqgc4qq6l2vqswzz5zq5v4r4x98jgyqd0sk2fae803crnevusngv9wq7jl8cf5e5eny56p4gpsrcjq4sfqgqqyzg74d7qxqqywkwkudz29aasp4cqtqggrc3nnudswp67znrydjtv7ta56c9cpc0nmjmv7rszs568gqdz3w770qsx3axhvq3e7npme2pwslgxa8kfcnqjqyeztg5r5wgzjpufjswx4crvd6kzlqjzukq5e707kp9ez98mj0zkckeggkm8cp6g6vgzh3j2q0lgp8ypt4ws")
|
||||
val Success(codedDecoded) = Bolt12Invoice.fromString(invoice.toString)
|
||||
assert(codedDecoded.chain == chain)
|
||||
assert(codedDecoded.offerId.contains(offerId))
|
||||
assert(codedDecoded.invoiceRequest.chain == chain)
|
||||
assert(codedDecoded.amount == amount)
|
||||
assert(codedDecoded.description == Left(description))
|
||||
assert(codedDecoded.features == features)
|
||||
assert(codedDecoded.issuer.contains(issuer))
|
||||
assert(codedDecoded.invoiceRequest.offer.issuer.contains(issuer))
|
||||
assert(codedDecoded.nodeId.value.drop(1) == nodeKey.publicKey.value.drop(1))
|
||||
assert(codedDecoded.blindedPaths == Seq(path))
|
||||
assert(codedDecoded.quantity.contains(quantity))
|
||||
assert(codedDecoded.payerKey.contains(payerKey))
|
||||
assert(codedDecoded.payerNote.contains(payerNote))
|
||||
assert(codedDecoded.payerInfo.contains(payerInfo))
|
||||
assert(codedDecoded.invoiceRequest.quantity == quantity)
|
||||
assert(codedDecoded.invoiceRequest.payerId == payerKey)
|
||||
assert(codedDecoded.invoiceRequest.payerNote.contains(payerNote))
|
||||
assert(codedDecoded.invoiceRequest.metadata == payerInfo)
|
||||
assert(codedDecoded.createdAt == createdAt)
|
||||
assert(codedDecoded.paymentHash == paymentHash)
|
||||
assert(codedDecoded.relativeExpiry == relativeExpiry.seconds)
|
||||
assert(codedDecoded.minFinalCltvExpiryDelta == cltv)
|
||||
assert(codedDecoded.fallbacks.contains(fallbacks))
|
||||
assert(codedDecoded.replaceInvoice.contains(replaceInvoice))
|
||||
assert(codedDecoded.records.unknown.toSet == Set(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex"")))
|
||||
assert(codedDecoded.records.unknown.toSet == Set(GenericTlv(UInt64(121), hex"010203"), GenericTlv(UInt64(313), hex"baba")))
|
||||
}
|
||||
|
||||
test("minimal tip") {
|
||||
|
@ -361,26 +261,26 @@ class Bolt12InvoiceSpec extends AnyFunSuite {
|
|||
assert(payerKey.publicKey == PublicKey(hex"031ef4439f638914de79220483dda32dfb7a431e799a5ce5a7643fbd70b2118e4e"))
|
||||
val preimage = ByteVector32(hex"317d1fd8fec5f3ea23044983c2ba2a8043395b2a0790a815c9b12719aa5f1516")
|
||||
val offer = Offer(None, "minimal tip", nodeKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
|
||||
val encodedOffer = "lno1pg9k66twd9kkzmpqw35hq83pqf8l2vtlq5w87m4vqfnvtn82adk9wadfgratnp2wg7l7ha4u0gzqw"
|
||||
val encodedOffer = "lno1pg9k66twd9kkzmpqw35hq93pqf8l2vtlq5w87m4vqfnvtn82adk9wadfgratnp2wg7l7ha4u0gzqw"
|
||||
assert(offer.toString == encodedOffer)
|
||||
assert(Offer.decode(encodedOffer).get == offer)
|
||||
val request = InvoiceRequest(offer, 12000000 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||
val encodedRequest = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyyrfgrkke8dp3jww26jz8zgvhxhdhzgj062ejxecv9uqsdhh2x9lnjzqrkudsqf3qrm6y88mr3y2du7fzqjpamgedldayx8nenfwwtfmy877hpvs33e80qszhudm9rdk99qpnzktv6emdwq3gda2l77c6av7nn542sl3uhzq5yau26508s7n0mf3ztpnwr6f8vlxhjrlhc34w6sehs9jwydpxhxnws"
|
||||
assert(request.toString == encodedRequest)
|
||||
assert(InvoiceRequest.decode(encodedRequest).get == request)
|
||||
// Invoice request generation is not reproducible because we add randomness in the first TLV.
|
||||
val encodedRequest = "lnr1qqs289chx8swkpmwf3uzexfxr0kk9syavsjcmkuur5qgjqt60ayjdec2pdkkjmnfd4skcgr5d9cpvggzfl6nzlc9r3lkatqzvmzue6htd3tht22ql2uc2nj8hl4ld0r6qsr4qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqpfq8dcmqpvzzqc773pe7cufzn08jgsys0w6xt0m0fp3u7v6tnj6weplh4ctyyvwfmcypemfjk6kryqxycnnmu2vp9tuw00eslf0grp6rf3hk6v76aynyn4lclra0fyyk2gxyf9hx73rnm775204tn8cltacw4s0fzd5c0lxm58s"
|
||||
val decodedRequest = InvoiceRequest.decode(encodedRequest).get
|
||||
assert(decodedRequest.unsigned.records.filterNot(_.isInstanceOf[InvoiceRequestMetadata]) == request.unsigned.records.filterNot(_.isInstanceOf[InvoiceRequestMetadata]))
|
||||
assert(request.isValidFor(offer))
|
||||
val invoice = Bolt12Invoice(offer, request, preimage, nodeKey, CltvExpiryDelta(22), Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
val invoice = Bolt12Invoice(decodedRequest, preimage, nodeKey, 300 seconds, Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
assert(Bolt12Invoice.fromString(invoice.toString).get.records == invoice.records)
|
||||
assert(invoice.isValidFor(offer, request))
|
||||
assert(invoice.isValidFor(decodedRequest))
|
||||
// Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same.
|
||||
val encodedInvoice = "lni1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyyrfgrkke8dp3jww26jz8zgvhxhdhzgj062ejxecv9uqsdhh2x9lnjzqrkudsqzstd45ku6tdv9kzqarfwqg8sqj075ch7pgu0ah2cqnxchxw46mv2a66js86hxz5u3ala0mtc7syqupdypsecj08jzgq82kzfmd8ncs9mufkaea9dr305na9vccycmjmlfspqvxsr2nmet6yjwzmjtrmqspxnyt9wl9jv46ep5t49amw3xpj82hk6qqjy0yn6ww6ektzyys7qrm6zcul88r27ysuqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpdcmqqqqq83pqf8l2vtlq5w87m4vqfnvtn82adk9wadfgratnp2wg7l7ha4u0gzqwf3qrm6y88mr3y2du7fzqjpamgedldayx8nenfwwtfmy877hpvs33e8zsprzmlnns23qshlyweee7p4m365legtkdgvy6s02rdqsv38mwnmk8p88cz03dt7zuqsqzmcyqvpkh2g4088w2xu7uvu6zvsxwrh2vgvppgnmf0vyqhqwqv6w8lgeulalcq6xznps7gw9h0rtfpwxftz4l7j2nnuzj3gpy86kg34awtdq"
|
||||
val encodedInvoice = "lni1qqs289chx8swkpmwf3uzexfxr0kk9syavsjcmkuur5qgjqt60ayjdec2pdkkjmnfd4skcgr5d9cpvggzfl6nzlc9r3lkatqzvmzue6htd3tht22ql2uc2nj8hl4ld0r6qsr4qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqpfq8dcmqpvzzqc773pe7cufzn08jgsys0w6xt0m0fp3u7v6tnj6weplh4ctyyvwf6s2qqj075ch7pgu0ah2cqnxchxw46mv2a66js86hxz5u3ala0mtc7syqup2a4g7lywy0zytzjzdhlar5uegx8qj8el2a2hpl7z30cv56fxkhwqpqgpnv93lzfep3m5ppkt3jry0kanpk3uxku733nr03snlzqjls3pejqp65tnf8nf8te9h67ge0lgzum5kypuvqrdz50t238n6g0wrdtv49nrgjk7k26rw7a24arfx9z4dup8379etdpw0tfkg3mwtngsuqqqqqqgqqqqqyqqrqqqqqqqqqqqqgqqqqqqqqqqqq5qqpfqyvwv9m2dxqgqje2pqshlyweee7p4m365legtkdgvy6s02rdqsv38mwnmk8p88cz03dt725qahrvqtqggzfl6nzlc9r3lkatqzvmzue6htd3tht22ql2uc2nj8hl4ld0r6qsrlqsxuf5rcjutppkh79vr6q7vma5yccxhf79ghfg5zkc6z4u3zqzyh0nf50g7w7q4gk32hqg97pn7p9kaz0ddm5fza65ztdqj2sry3gw6l2"
|
||||
val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get
|
||||
assert(decodedInvoice.amount == invoice.amount)
|
||||
assert(decodedInvoice.nodeId == invoice.nodeId)
|
||||
assert(decodedInvoice.paymentHash == invoice.paymentHash)
|
||||
assert(decodedInvoice.description == invoice.description)
|
||||
assert(decodedInvoice.payerKey == invoice.payerKey)
|
||||
assert(decodedInvoice.chain == invoice.chain)
|
||||
assert(decodedInvoice.invoiceRequest.unsigned == invoice.invoiceRequest.unsigned)
|
||||
}
|
||||
|
||||
test("minimal offer") {
|
||||
|
@ -390,26 +290,26 @@ class Bolt12InvoiceSpec extends AnyFunSuite {
|
|||
assert(payerKey.publicKey == PublicKey(hex"033e94f2afd568d128f02ece844ad4a0a1ddf2a4e3a08beb2dba11b3f1134b0517"))
|
||||
val preimage = ByteVector32(hex"09ad5e952ec39d45461ebdeceac206fb45574ae9054b5a454dd02c65f5ba1b7c")
|
||||
val offer = Offer(Some(456000000 msat), "minimal offer", nodeKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
|
||||
val encodedOffer = "lno1pqzpktszqq9q6mtfde5k6ctvyphkven9wg0zzq7y3tyhuz0newawkdds924x6pet2aexssdrf5je2g2het9xpgw275"
|
||||
val encodedOffer = "lno1pqzpktszqq9q6mtfde5k6ctvyphkven9wgtzzq7y3tyhuz0newawkdds924x6pet2aexssdrf5je2g2het9xpgw275"
|
||||
assert(offer.toString == encodedOffer)
|
||||
assert(Offer.decode(encodedOffer).get == offer)
|
||||
val request = InvoiceRequest(offer, 456001234 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash)
|
||||
val encodedRequest = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypmpsc7ww3cxguwl27ela95ykset7t8tlvyfy7a200eujcnhczws6zqyrvhqd53xyqlffu40645dz28s9m8ggjk55zsamu4yuwsgh6edhggm8ugnfvz30uzqjq9tsnv60r570yqfypx3jghrff92qlcjwff0azwatsuehd0vkxxvz2wx07qlurz42ca0r96x6a4xh5h9gpz39w4em3687k6n3w9349g"
|
||||
assert(request.toString == encodedRequest)
|
||||
assert(InvoiceRequest.decode(encodedRequest).get == request)
|
||||
// Invoice request generation is not reproducible because we add randomness in the first TLV.
|
||||
val encodedRequest = "lnr1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh7pq2hlvh7twke5830a44wc0zlrs2kph4ghndm60ahwcznhcd0pcpl332qv5xuemksazy3zx5s63kqmqkphrn9jg4ln55pc6syrwqukejeq"
|
||||
val decodedRequest = InvoiceRequest.decode(encodedRequest).get
|
||||
assert(decodedRequest.unsigned.records.filterNot(_.isInstanceOf[InvoiceRequestMetadata]) == request.unsigned.records.filterNot(_.isInstanceOf[InvoiceRequestMetadata]))
|
||||
assert(request.isValidFor(offer))
|
||||
val invoice = Bolt12Invoice(offer, request, preimage, nodeKey, CltvExpiryDelta(22), Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
val invoice = Bolt12Invoice(decodedRequest, preimage, nodeKey, 300 seconds, Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
assert(Bolt12Invoice.fromString(invoice.toString).get.records == invoice.records)
|
||||
assert(invoice.isValidFor(offer, request))
|
||||
assert(invoice.isValidFor(decodedRequest))
|
||||
// Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same.
|
||||
val encodedInvoice = "lni1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypmpsc7ww3cxguwl27ela95ykset7t8tlvyfy7a200eujcnhczws6zqyrvhqd5s2p4kkjmnfd4skcgr0venx2ussnqpufzkf0cyl8ja6av6mq242d5rjk4mjdpq6xnf9j5s40jk2vzsu4agr8f5tqgegums2pxkyxcarfk6fyzdk37akrn808xrptvzzj222gv9szqervpzvaxzejaejwul8wkjuldd0qpjxpt85vlp3mncpyx30dgrzduqr99dq04sehw2nh3kqcnmj87gn9x5fcln9njcshnjcqc4c4d9vvw98fxeqm2037p4e82jce87n6nud6gncvysuqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpktsx6gqqq83pq0zg4jt7p8euhwhtxkcz42ndqu44wunggx356fv4y9tu4jnq58902f3q86209t74drgj3upwe6zy449q58wl9f8r5z97ktd6zxelzy6tq5tjsprzmlnns23q9jcw0vzjxencw3gvx0d0d5hjc09kzv3zzvnwrsd5ntyhlht7kuszuqsqzmcyqh2ej2lvwj9chganv56tasj2a4x9expx44tr65u9cw8xyrdzvqnd09g60evuy5gqs08hxmx4rd2npqfdekmqjc4zvf5qf0v65uta9glq"
|
||||
val encodedInvoice = "lni1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh5zsq83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4qfjynufc627cuspz9lqzyk387xgzs4txcw0q97ugxfqm8x5zgj02gqgz4mnucmtxr620e5ttewtsg0s5n88euljnf7puagqje9j6gvaxk3pqqwsmahw79nhuq05zh8k29jk5qngpuny5l2vhjdrexg8hejukaee8fr7963dfag9q3lpcq9tt23f8s4h89cmjqa43u4fhk6l2y8qqqqqqzqqqqqpqqqcqqqqqqqqqqqzqqqqqqqqqqqq9qqq2gprrnp0zefszqyk2sgpvkrnmq53kv7r52rpnmtmd9ukredsnygsnymsurdy6e9la6l4hyz4qgxewqmftqggrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet6lqsy3s64amqgnlel7hn6fjrnk32xrn0ugr2xzct22ew28zftgmj70q9x2akqm34que8u2qe643cm38jpka6nfca4lfhuq6hgpnpwkpexrc"
|
||||
val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get
|
||||
assert(decodedInvoice.amount == invoice.amount)
|
||||
assert(decodedInvoice.nodeId == invoice.nodeId)
|
||||
assert(decodedInvoice.paymentHash == invoice.paymentHash)
|
||||
assert(decodedInvoice.description == invoice.description)
|
||||
assert(decodedInvoice.payerKey == invoice.payerKey)
|
||||
assert(decodedInvoice.chain == invoice.chain)
|
||||
assert(decodedInvoice.invoiceRequest.unsigned == invoice.invoiceRequest.unsigned)
|
||||
}
|
||||
|
||||
test("offer with quantity") {
|
||||
|
@ -419,32 +319,41 @@ class Bolt12InvoiceSpec extends AnyFunSuite {
|
|||
assert(payerKey.publicKey == PublicKey(hex"027c6d03fa8f366e2ef8017cdfaf5d3cf1a3b0123db1318263b662c0aa9ec9c959"))
|
||||
val preimage = ByteVector32(hex"99221825b86576e94391b179902be8b22c7cfa7c3d14aec6ae86657dfd9bd2a8")
|
||||
val offer = Offer(TlvStream[OfferTlv](
|
||||
Chains(Seq(Block.TestnetGenesisBlock.hash)),
|
||||
Amount(100000 msat),
|
||||
Description("offer with quantity"),
|
||||
Issuer("alice@bigshop.com"),
|
||||
QuantityMin(50),
|
||||
QuantityMax(1000),
|
||||
NodeId(nodeKey.publicKey)))
|
||||
val encodedOffer = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2zdhkven9wgs8w6t5dqs8zatpde6xjarezsgkzmrfvdj5qcnfvaeksmms9e3k7mgkqyepsqsraq0zzqe84l2enk3jym6xpzuk4vzzle2cha2cyywnchn8anytaxtrygzrfu"
|
||||
OfferChains(Seq(Block.TestnetGenesisBlock.hash)),
|
||||
OfferAmount(100000 msat),
|
||||
OfferDescription("offer with quantity"),
|
||||
OfferIssuer("alice@bigshop.com"),
|
||||
OfferQuantityMax(1000),
|
||||
OfferNodeId(nodeKey.publicKey)))
|
||||
val encodedOffer = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2zdhkven9wgs8w6t5dqs8zatpde6xjarezggkzmrfvdj5qcnfvaeksmms9e3k7mg5qgp7s93pqvn6l4vemgezdarq3wt2kpp0u4vt74vzz8futen7ej97n93jypp57"
|
||||
assert(offer.toString == encodedOffer)
|
||||
assert(Offer.decode(encodedOffer).get == offer)
|
||||
val request = InvoiceRequest(offer, 7200000 msat, 72, Features.empty, payerKey, Block.TestnetGenesisBlock.hash)
|
||||
val encodedRequest = "lnr1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyqcnw8ucesh0ttrka67a62qyf04tsprv4ul6uyrpctdm596q7av2zzqrdhwsqgqpfqnzqlrdq0ag7dnw9muqzlxl4awneudrkqfrmvf3sf3mvckq420vnj2e7pq998np5gs3khqdqpgztenk5k5wqhzlxjg0ed4q9439yh8dayzz7q24kay7qsrhxg8tf303g223fknj8d79d3dvj78nlkg9s8c5hyqgz5"
|
||||
assert(request.toString == encodedRequest)
|
||||
assert(InvoiceRequest.decode(encodedRequest).get == request)
|
||||
// Invoice request generation is not reproducible because we add randomness in the first TLV.
|
||||
val encodedRequest = "lnr1qqs8lqvnh3kg9uj003lxlxyj8hthymgq4p9ms0ag0ryx5uw8gsuus4gzypp5jl7hlqnf2ugg7j3slkwwcwht57vhyzzwjr4dq84rxzgqqqqqqzqrqxr2qzsndanxvetjypmkjargypch2ctww35hg7gjz9skc6trv4qxy6t8wd5x7upwvdhk69qzq05pvggry7hatxw6xgn0gcytj64sgtl9tzl4tqs360z7vlkv305evv3qgd84qgzrf9la07pxj4cs3a9rplvuasawhfuewgyyay826q02xvysqqqqqpfqxmwaqptqzjzcyyp8cmgrl28nvm3wlqqheha0t570rgaszg7mzvvzvwmx9s92nmyujk0sgpef8dt57nygu3dnfhglymt6mnle6j8s28rler8wv3zygen07v4ddfplc9qs7nkdzwcelm2rs552slkpv45xxng65ne6y4dlq2764gqv"
|
||||
val decodedRequest = InvoiceRequest.decode(encodedRequest).get
|
||||
assert(decodedRequest.unsigned.records.filterNot(_.isInstanceOf[InvoiceRequestMetadata]) == request.unsigned.records.filterNot(_.isInstanceOf[InvoiceRequestMetadata]))
|
||||
assert(request.isValidFor(offer))
|
||||
val invoice = Bolt12Invoice(offer, request, preimage, nodeKey, CltvExpiryDelta(34), Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
val invoice = Bolt12Invoice(decodedRequest, preimage, nodeKey, 300 seconds, Features.empty, Seq(createPaymentBlindedRoute(nodeKey.publicKey)))
|
||||
assert(Bolt12Invoice.fromString(invoice.toString).get.records == invoice.records)
|
||||
assert(invoice.isValidFor(offer, request))
|
||||
assert(invoice.isValidFor(decodedRequest))
|
||||
// Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same.
|
||||
val encodedInvoice = "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyqcnw8ucesh0ttrka67a62qyf04tsprv4ul6uyrpctdm596q7av2zzqrdhwsqzsndanxvetjypmkjargypch2ctww35hg7gsnqpj0t74n8dryfh5vz9ed2cy9lj43064sgga830x0mxgh6vkxgsyxnczy9ysc4m9zqvmruq7clt4dfxuwjn8hmc240m0pm4yclacwtkugtaqzq75v83x5evkfwaj4amaac7e84kf9l6zcr28nyv7mx09jv87zvdvcuqr9d5ex7wdrd3g7vjxjnztctuk2tuasa5xs8klwadygqaq5dtner75zpfmptt0jv7mha7s60gft0nh8efmcysuqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmwaqqqqq9q3v9kxjcm9gp3xjemndphhqtnrdak3uggry7hatxw6xgn0gcytj64sgtl9tzl4tqs360z7vlkv305evv3qgd8jqq2gycs8cmgrl28nvm3wlqqheha0t570rgaszg7mzvvzvwmx9s92nmyujkfgq33dleec9gs8up5r8hpz5vcfzxv706ag9yrde627yfhscttac8lw9u5u3g3udvpwqgqz9uzqt5ag0q6zkyft7jwxxcgr9etqk2psjcc44rzye2yzvx5mw7qw694lzka89xnn49qt6yh8am5xtdr5jy3mkzg49xwnz2zvx2z3a7rdajg"
|
||||
val encodedInvoice = "lni1qqs8lqvnh3kg9uj003lxlxyj8hthymgq4p9ms0ag0ryx5uw8gsuus4gzypp5jl7hlqnf2ugg7j3slkwwcwht57vhyzzwjr4dq84rxzgqqqqqqzqrqxr2qzsndanxvetjypmkjargypch2ctww35hg7gjz9skc6trv4qxy6t8wd5x7upwvdhk69qzq05pvggry7hatxw6xgn0gcytj64sgtl9tzl4tqs360z7vlkv305evv3qgd84qgzrf9la07pxj4cs3a9rplvuasawhfuewgyyay826q02xvysqqqqqpfqxmwaqptqzjzcyyp8cmgrl28nvm3wlqqheha0t570rgaszg7mzvvzvwmx9s92nmyujkdq5qpj0t74n8dryfh5vz9ed2cy9lj43064sgga830x0mxgh6vkxgsyxnczgew6pkkhja3cl3dfxthumcmp6gkp446ha4tcj884eqch6g57newqzquqmar5nynwtg9lknq98yzslwla3vdxefulhq2jkwnqnsf7umpl5cqr58qkj63hkpl7ffyd6f3qgn3m5kuegehhakvxw7fuw29tf3r5wgj37uecjdw2th4t5fp7f99xvk4f3gwl0wyf2a558wqa9w3pcqqqqqqsqqqqqgqqxqqqqqqqqqqqqsqqqqqqqqqqqpgqqzjqgcuctck2vqsp9j5zqlsxsv7uy23npygenelt4q5sdh8ftc3x7rpd0hqlachjnj9z834s4gpkmhgqkqssxfa06kva5v3x73sgh94tqsh72k9l2kppr579uelvezlfjcezqs607pqxa3afljxyf2ua9dlqs33wrfzakt5tpraklpzfpn63uxa7el475x4sc0w4hs75e3nhe689slfz4ldqlwja3zaq0w3mnz79f4ne0c3r3c"
|
||||
val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get
|
||||
assert(decodedInvoice.amount == invoice.amount)
|
||||
assert(decodedInvoice.nodeId == invoice.nodeId)
|
||||
assert(decodedInvoice.paymentHash == invoice.paymentHash)
|
||||
assert(decodedInvoice.description == invoice.description)
|
||||
assert(decodedInvoice.payerKey == invoice.payerKey)
|
||||
assert(decodedInvoice.chain == invoice.chain)
|
||||
assert(decodedInvoice.invoiceRequest.unsigned == invoice.invoiceRequest.unsigned)
|
||||
}
|
||||
|
||||
test("cln invoice"){
|
||||
val encodedInvoice = "lni1qqgds4gweqxey37gexf5jus4kcrwuq3qqc3xu3s3rg94nj40zfsy866mhu5vxne6tcej5878k2mneuvgjy8s5predakx793pqfxv2rtqfajhp98c5tlsxxkkmzy0ntpzp2rtt9yum2495hqrq4wkj5pqqc3xu3s3rg94nj40zfsy866mhu5vxne6tcej5878k2mneuvgjy84yqucj6q9sggrnl24r93kfmdnatwpy72mxg7ygr9waxu0830kkpqx84pd5j65fhg2pxqzfnzs6cz0v4cff79zlup344kc3ru6cgs2s66ef8x64fd9cqc9t45s954fef6n3ql8urpc4r2vvunc0uv9yq37g485heph6lpuw34ywxadqypwq3hlcrpyk32zdvlrgfsdnx5jegumenll49v502862l9sq5erz3qqxte8tyk308ykd6fqy2lxkrsmeq77d8s5977pzmc68lgvs2xcn0kfvnlzud9fvkv900ggwe7yf9hf7lr6qz3pcqqqqqqqqqqqqqqq5qqqqqqqqqqqqqwjfvkl43fqqqqqqzjqgcuhrdv2sgq5spd8qp4ev2rw0v9r7cvvrntlzpvlwmd8vczycklu87336h55g24q8xykszczzqjvc5xkqnm9wz203ghlqvdddkyglxkzyz5xkk2fek42tfwqxp2ad8cypv26x5zxkyk675ep3v48grwydze6nvvg56cklgmvztuny58t5j0fl3hemx3lvd0ryx89jtf0h069z6r2qwqvjlyrewvzsfqmmfajs70q"
|
||||
val invoice = Bolt12Invoice.fromString(encodedInvoice).get
|
||||
assert(invoice.checkSignature())
|
||||
assert(invoice.amount == 10000000.msat)
|
||||
assert(invoice.nodeId == PublicKey(hex"024cc50d604f657094f8a2ff031ad6d888f9ac220a86b5949cdaaa5a5c03055d69"))
|
||||
assert(invoice.paymentHash == ByteVector32(hex"14805a7006b96286e7b0a3f618c1cd7f1059f76da766044c5bfc3fa31d5e9442"))
|
||||
assert(invoice.description == Left("yolo"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,9 +95,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
}
|
||||
}
|
||||
|
||||
def createBlindedPacket(amount: MilliSatoshi, paymentHash: ByteVector32, expiry: CltvExpiry, pathId: ByteVector, blinding_opt: Option[PublicKey]): IncomingPaymentPacket.FinalPacket = {
|
||||
def createBlindedPacket(amount: MilliSatoshi, paymentHash: ByteVector32, expiry: CltvExpiry, finalExpiry: CltvExpiry, pathId: ByteVector, blinding_opt: Option[PublicKey]): IncomingPaymentPacket.FinalPacket = {
|
||||
val add = UpdateAddHtlc(ByteVector32.One, 0, amount, paymentHash, expiry, TestConstants.emptyOnionPacket, blinding_opt)
|
||||
val payload = FinalPayload.Blinded(TlvStream(AmountToForward(amount), TotalAmount(amount), OutgoingCltv(expiry), EncryptedRecipientData(hex"deadbeef")), TlvStream(PathId(pathId), PaymentConstraints(CltvExpiry(500_000), 1 msat)))
|
||||
val payload = FinalPayload.Blinded(TlvStream(AmountToForward(amount), TotalAmount(amount), OutgoingCltv(finalExpiry), EncryptedRecipientData(hex"deadbeef")), TlvStream(PathId(pathId), PaymentConstraints(CltvExpiry(500_000), 1 msat)))
|
||||
IncomingPaymentPacket.FinalPacket(add, payload)
|
||||
}
|
||||
|
||||
|
@ -164,16 +164,16 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
{
|
||||
val privKey = randomKey()
|
||||
val offer = Offer(Some(amountMsat), "a blinded coffee please", privKey.publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, amountMsat, 1, featuresWithRouteBlinding.invoiceFeatures(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, amountMsat, 1, featuresWithRouteBlinding.bolt12Features(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
val router = TestProbe()
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(privKey, offer, invoiceReq, createEmptyReceivingRoute(), router.ref))
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(privKey, invoiceReq, createEmptyReceivingRoute(), router.ref))
|
||||
router.expectNoMessage(50 millis)
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory)))
|
||||
val pendingPayment = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.asInstanceOf[IncomingBlindedPayment]
|
||||
assert(pendingPayment.status == IncomingPaymentStatus.Pending)
|
||||
|
||||
val finalPacket = createBlindedPacket(amountMsat, invoice.paymentHash, defaultExpiry, pendingPayment.pathIds.values.head, pendingPayment.pathIds.keys.headOption)
|
||||
val finalPacket = createBlindedPacket(amountMsat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), pendingPayment.pathIds.values.head, pendingPayment.pathIds.keys.headOption)
|
||||
sender.send(handlerWithRouteBlinding, finalPacket)
|
||||
assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == finalPacket.add.id)
|
||||
|
||||
|
@ -280,7 +280,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
val privKey = randomKey()
|
||||
val offer = Offer(Some(25_000 msat), "a blinded coffee please", privKey.publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 25_000 msat, 1, featuresWithRouteBlinding.invoiceFeatures(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 25_000 msat, 1, featuresWithRouteBlinding.bolt12Features(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
val router = TestProbe()
|
||||
val (a, b, c, d) = (randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, nodeParams.nodeId)
|
||||
val hop_ab = Router.ChannelHop(ShortChannelId(1), a, b, Router.HopRelayParams.FromHint(Invoice.ExtraEdge(a, b, ShortChannelId(1), 1000 msat, 0, CltvExpiryDelta(100), 1 msat, None)))
|
||||
|
@ -291,7 +291,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
ReceivingRoute(Seq(c, d), CltvExpiryDelta(50), Seq(DummyBlindedHop(250 msat, 0, CltvExpiryDelta(10)), DummyBlindedHop(150 msat, 0, CltvExpiryDelta(80)))),
|
||||
ReceivingRoute(Seq(d), CltvExpiryDelta(250)),
|
||||
)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(privKey, offer, invoiceReq, receivingRoutes, router.ref))
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(privKey, invoiceReq, receivingRoutes, router.ref))
|
||||
val finalizeRoute1 = router.expectMsgType[Router.FinalizeRoute]
|
||||
assert(finalizeRoute1.route == Router.PredefinedNodeRoute(25_000 msat, Seq(a, b, d)))
|
||||
router.send(router.lastSender, RouteResponse(Seq(Router.Route(25_000 msat, Seq(hop_ab, hop_bd), None))))
|
||||
|
@ -304,20 +304,20 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(invoice.blindedPaths.nonEmpty)
|
||||
assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory)))
|
||||
assert(invoice.description == Left("a blinded coffee please"))
|
||||
assert(invoice.offerId.contains(offer.offerId))
|
||||
assert(invoice.invoiceRequest.offer == offer)
|
||||
assert(invoice.blindedPaths.length == 3)
|
||||
assert(invoice.blindedPaths(0).blindedNodeIds.length == 4)
|
||||
assert(invoice.blindedPaths(0).introductionNodeId == a)
|
||||
assert(invoice.blindedPathsInfo(0) == PaymentInfo(1950 msat, 0, CltvExpiryDelta(175), 1 msat, 25_000 msat, Features.empty))
|
||||
assert(invoice.blindedPaths(1).blindedNodeIds.length == 4)
|
||||
assert(invoice.blindedPaths(1).introductionNodeId == c)
|
||||
assert(invoice.blindedPathsInfo(1) == PaymentInfo(400 msat, 0, CltvExpiryDelta(165), 1 msat, 25_000 msat, Features.empty))
|
||||
assert(invoice.blindedPaths(2).blindedNodeIds.length == 1)
|
||||
assert(invoice.blindedPaths(2).introductionNodeId == d)
|
||||
assert(invoice.blindedPathsInfo(2) == PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 25_000 msat, Features.empty))
|
||||
assert(invoice.blindedPaths(0).route.blindedNodeIds.length == 4)
|
||||
assert(invoice.blindedPaths(0).route.introductionNodeId == a)
|
||||
assert(invoice.blindedPaths(0).paymentInfo == PaymentInfo(1950 msat, 0, CltvExpiryDelta(193), 1 msat, 25_000 msat, Features.empty))
|
||||
assert(invoice.blindedPaths(1).route.blindedNodeIds.length == 4)
|
||||
assert(invoice.blindedPaths(1).route.introductionNodeId == c)
|
||||
assert(invoice.blindedPaths(1).paymentInfo == PaymentInfo(400 msat, 0, CltvExpiryDelta(183), 1 msat, 25_000 msat, Features.empty))
|
||||
assert(invoice.blindedPaths(2).route.blindedNodeIds.length == 1)
|
||||
assert(invoice.blindedPaths(2).route.introductionNodeId == d)
|
||||
assert(invoice.blindedPaths(2).paymentInfo == PaymentInfo(0 msat, 0, CltvExpiryDelta(18), 0 msat, 25_000 msat, Features.empty))
|
||||
|
||||
val pendingPayment = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.asInstanceOf[IncomingBlindedPayment]
|
||||
assert(pendingPayment.invoice == invoice)
|
||||
assert(pendingPayment.invoice.toString == invoice.toString)
|
||||
assert(pendingPayment.status == IncomingPaymentStatus.Pending)
|
||||
assert(pendingPayment.pathIds.nonEmpty)
|
||||
pendingPayment.pathIds.values.foreach(pathId => assert(pathId.length == 32))
|
||||
|
@ -328,7 +328,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
val privKey = randomKey()
|
||||
val offer = Offer(Some(25_000 msat), "a blinded coffee please", privKey.publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 25_000 msat, 1, featuresWithRouteBlinding.invoiceFeatures(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 25_000 msat, 1, featuresWithRouteBlinding.bolt12Features(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
val router = TestProbe()
|
||||
val (a, b, c) = (randomKey().publicKey, randomKey().publicKey, nodeParams.nodeId)
|
||||
val hop_ac = Router.ChannelHop(ShortChannelId(1), a, c, Router.HopRelayParams.FromHint(Invoice.ExtraEdge(a, c, ShortChannelId(1), 100 msat, 0, CltvExpiryDelta(50), 1 msat, None)))
|
||||
|
@ -336,7 +336,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
ReceivingRoute(Seq(a, c), CltvExpiryDelta(100)),
|
||||
ReceivingRoute(Seq(b, c), CltvExpiryDelta(100)),
|
||||
)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(privKey, offer, invoiceReq, receivingRoutes, router.ref))
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(privKey, invoiceReq, receivingRoutes, router.ref))
|
||||
val finalizeRoute1 = router.expectMsgType[Router.FinalizeRoute]
|
||||
assert(finalizeRoute1.route == Router.PredefinedNodeRoute(25_000 msat, Seq(a, c)))
|
||||
router.send(router.lastSender, RouteResponse(Seq(Router.Route(25_000 msat, Seq(hop_ac), None))))
|
||||
|
@ -491,7 +491,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
val invoice = sender.expectMsgType[Bolt11Invoice]
|
||||
assert(!invoice.features.hasFeature(RouteBlinding))
|
||||
|
||||
val packet = createBlindedPacket(1000 msat, invoice.paymentHash, defaultExpiry, hex"deadbeef", Some(randomKey().publicKey))
|
||||
val packet = createBlindedPacket(1000 msat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), hex"deadbeef", Some(randomKey().publicKey))
|
||||
sender.send(handlerWithRouteBlinding, packet)
|
||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||
|
@ -501,9 +501,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
test("PaymentHandler should reject incoming standard payment for Bolt 12 invoice") { f =>
|
||||
import f._
|
||||
|
||||
val offer = Offer(None, "a blinded coffee please", randomKey().publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.invoiceFeatures(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(randomKey(), offer, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val nodeKey = randomKey()
|
||||
val offer = Offer(None, "a blinded coffee please", nodeKey.publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.bolt12Features(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(nodeKey, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory)))
|
||||
|
||||
|
@ -517,15 +518,16 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
test("PaymentHandler should accept incoming blinded payment with correct blinding point and path id") { f =>
|
||||
import f._
|
||||
|
||||
val offer = Offer(None, "a blinded coffee please", randomKey().publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.invoiceFeatures(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(randomKey(), offer, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val nodeKey = randomKey()
|
||||
val offer = Offer(None, "a blinded coffee please", nodeKey.publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.bolt12Features(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(nodeKey, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory)))
|
||||
val pathIds = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.asInstanceOf[IncomingBlindedPayment].pathIds
|
||||
assert(pathIds.size == 1)
|
||||
|
||||
val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, pathIds.values.head, Some(pathIds.keys.head))
|
||||
val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), pathIds.values.head, Some(pathIds.keys.head))
|
||||
sender.send(handlerWithRouteBlinding, packet)
|
||||
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message
|
||||
assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status.isInstanceOf[IncomingPaymentStatus.Received])
|
||||
|
@ -534,16 +536,17 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
test("PaymentHandler should accept incoming blinded payment with correct blinding point and path id (zero hop)") { f =>
|
||||
import f._
|
||||
|
||||
val offer = Offer(None, "a blinded coffee please", randomKey().publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.invoiceFeatures(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(randomKey(), offer, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val nodeKey = randomKey()
|
||||
val offer = Offer(None, "a blinded coffee please", nodeKey.publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.bolt12Features(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(nodeKey, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory)))
|
||||
val pathIds = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.asInstanceOf[IncomingBlindedPayment].pathIds
|
||||
assert(pathIds.size == 1)
|
||||
|
||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 5000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None)
|
||||
val payload = FinalPayload.Blinded(TlvStream(BlindingPoint(pathIds.keys.head), AmountToForward(5000 msat), TotalAmount(5000 msat), OutgoingCltv(defaultExpiry), EncryptedRecipientData(hex"deadbeef")), TlvStream(PathId(pathIds.values.head), PaymentConstraints(CltvExpiry(500_000), 1 msat)))
|
||||
val payload = FinalPayload.Blinded(TlvStream(BlindingPoint(pathIds.keys.head), AmountToForward(5000 msat), TotalAmount(5000 msat), OutgoingCltv(CltvExpiry(nodeParams.currentBlockHeight)), EncryptedRecipientData(hex"deadbeef")), TlvStream(PathId(pathIds.values.head), PaymentConstraints(CltvExpiry(500_000), 1 msat)))
|
||||
val packet = IncomingPaymentPacket.FinalPacket(add, payload)
|
||||
sender.send(handlerWithRouteBlinding, packet)
|
||||
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message
|
||||
|
@ -553,15 +556,16 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
test("PaymentHandler should reject incoming blinded payment without a blinding point") { f =>
|
||||
import f._
|
||||
|
||||
val offer = Offer(None, "a blinded coffee please", randomKey().publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.invoiceFeatures(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(randomKey(), offer, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val nodeKey = randomKey()
|
||||
val offer = Offer(None, "a blinded coffee please", nodeKey.publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.bolt12Features(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(nodeKey, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory)))
|
||||
val pathIds = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.asInstanceOf[IncomingBlindedPayment].pathIds
|
||||
assert(pathIds.size == 1)
|
||||
|
||||
val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, pathIds.values.head, None)
|
||||
val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), pathIds.values.head, None)
|
||||
sender.send(handlerWithRouteBlinding, packet)
|
||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight)))
|
||||
|
@ -571,15 +575,16 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
test("PaymentHandler should reject incoming blinded payment with an invalid blinding point") { f =>
|
||||
import f._
|
||||
|
||||
val offer = Offer(None, "a blinded coffee please", randomKey().publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.invoiceFeatures(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(randomKey(), offer, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val nodeKey = randomKey()
|
||||
val offer = Offer(None, "a blinded coffee please", nodeKey.publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.bolt12Features(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(nodeKey, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory)))
|
||||
val pathIds = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.asInstanceOf[IncomingBlindedPayment].pathIds
|
||||
assert(pathIds.size == 1)
|
||||
|
||||
val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, pathIds.values.head, Some(randomKey().publicKey))
|
||||
val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), pathIds.values.head, Some(randomKey().publicKey))
|
||||
sender.send(handlerWithRouteBlinding, packet)
|
||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight)))
|
||||
|
@ -589,15 +594,35 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
test("PaymentHandler should reject incoming blinded payment with an invalid path id") { f =>
|
||||
import f._
|
||||
|
||||
val offer = Offer(None, "a blinded coffee please", randomKey().publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.invoiceFeatures(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(randomKey(), offer, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val nodeKey = randomKey()
|
||||
val offer = Offer(None, "a blinded coffee please", nodeKey.publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.bolt12Features(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(nodeKey, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory)))
|
||||
val pathIds = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.asInstanceOf[IncomingBlindedPayment].pathIds
|
||||
assert(pathIds.size == 1)
|
||||
|
||||
val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, hex"deadbeef", pathIds.keys.headOption)
|
||||
val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), hex"deadbeef", pathIds.keys.headOption)
|
||||
sender.send(handlerWithRouteBlinding, packet)
|
||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight)))
|
||||
assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending)
|
||||
}
|
||||
|
||||
test("PaymentHandler should reject incoming blinded payment with unexpected expiry") { f =>
|
||||
import f._
|
||||
|
||||
val nodeKey = randomKey()
|
||||
val offer = Offer(None, "a blinded coffee please", nodeKey.publicKey, Features.empty, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceReq = InvoiceRequest(offer, 5000 msat, 1, featuresWithRouteBlinding.bolt12Features(), randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
sender.send(handlerWithRouteBlinding, ReceiveOfferPayment(nodeKey, invoiceReq, createEmptyReceivingRoute(), TestProbe().ref))
|
||||
val invoice = sender.expectMsgType[Bolt12Invoice]
|
||||
assert(invoice.features.hasFeature(RouteBlinding, Some(Mandatory)))
|
||||
val pathIds = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.asInstanceOf[IncomingBlindedPayment].pathIds
|
||||
assert(pathIds.size == 1)
|
||||
|
||||
val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight) + CltvExpiryDelta(1), pathIds.values.head, pathIds.keys.headOption)
|
||||
sender.send(handlerWithRouteBlinding, packet)
|
||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight)))
|
||||
|
|
|
@ -36,7 +36,7 @@ import fr.acinq.eclair.router.Router._
|
|||
import fr.acinq.eclair.router.{BlindedRouteCreation, RouteNotFound}
|
||||
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer}
|
||||
import fr.acinq.eclair.wire.protocol._
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, PaymentFinalExpiryConf, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey}
|
||||
import fr.acinq.eclair.{Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshiLong, NodeParams, PaymentFinalExpiryConf, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey}
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import org.scalatest.{Outcome, Tag}
|
||||
import scodec.bits.{ByteVector, HexStringSyntax}
|
||||
|
@ -58,27 +58,27 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe)
|
||||
|
||||
val featuresWithoutMpp: Features[InvoiceFeature] = Features(
|
||||
val featuresWithoutMpp: Features[Bolt11Feature] = Features(
|
||||
VariableLengthOnion -> Mandatory,
|
||||
PaymentSecret -> Mandatory,
|
||||
RouteBlinding -> Optional,
|
||||
)
|
||||
|
||||
val featuresWithMpp: Features[InvoiceFeature] = Features(
|
||||
val featuresWithMpp: Features[Bolt11Feature] = Features(
|
||||
VariableLengthOnion -> Mandatory,
|
||||
PaymentSecret -> Mandatory,
|
||||
BasicMultiPartPayment -> Optional,
|
||||
RouteBlinding -> Optional,
|
||||
)
|
||||
|
||||
val featuresWithTrampoline: Features[InvoiceFeature] = Features(
|
||||
val featuresWithTrampoline: Features[Bolt11Feature] = Features(
|
||||
VariableLengthOnion -> Mandatory,
|
||||
PaymentSecret -> Mandatory,
|
||||
BasicMultiPartPayment -> Optional,
|
||||
TrampolinePaymentPrototype -> Optional,
|
||||
)
|
||||
|
||||
val featuresWithoutRouteBlinding: Features[InvoiceFeature] = Features(
|
||||
val featuresWithoutRouteBlinding: Features[Bolt11Feature] = Features(
|
||||
VariableLengthOnion -> Mandatory,
|
||||
PaymentSecret -> Mandatory,
|
||||
BasicMultiPartPayment -> Optional,
|
||||
|
@ -130,7 +130,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(payment.amount == finalAmount)
|
||||
assert(payment.recipient.nodeId == invoice.nodeId)
|
||||
assert(payment.recipient.totalAmount == finalAmount)
|
||||
assert(payment.recipient.expiry == req.invoice.minFinalCltvExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1))
|
||||
assert(payment.recipient.expiry == invoice.minFinalCltvExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1))
|
||||
assert(payment.recipient.isInstanceOf[ClearRecipient])
|
||||
assert(payment.recipient.asInstanceOf[ClearRecipient].customTlvs == customRecords)
|
||||
}
|
||||
|
@ -292,17 +292,17 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
sender.expectMsg(NoPendingPayment(Right(invoice.paymentHash)))
|
||||
}
|
||||
|
||||
def createBolt12Invoice(features: Features[InvoiceFeature]): Bolt12Invoice = {
|
||||
def createBolt12Invoice(features: Features[Bolt12Feature]): Bolt12Invoice = {
|
||||
val offer = Offer(None, "Bolt12 r0cks", e, features, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceRequest = InvoiceRequest(offer, finalAmount, 1, features, randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
val blindedRoute = BlindedRouteCreation.createBlindedRouteWithoutHops(e, hex"2a2a2a2a", 1 msat, CltvExpiry(500_000)).route
|
||||
val paymentInfo = OfferTypes.PaymentInfo(1_000 msat, 0, CltvExpiryDelta(24), 0 msat, finalAmount, Features.empty)
|
||||
Bolt12Invoice(offer, invoiceRequest, paymentPreimage, priv_e.privateKey, CltvExpiryDelta(6), features, Seq(PaymentBlindedRoute(blindedRoute, paymentInfo)))
|
||||
Bolt12Invoice(invoiceRequest, paymentPreimage, priv_e.privateKey, 300 seconds, features, Seq(PaymentBlindedRoute(blindedRoute, paymentInfo)))
|
||||
}
|
||||
|
||||
test("forward single-part blinded payment") { f =>
|
||||
import f._
|
||||
val invoice = createBolt12Invoice(Features(VariableLengthOnion -> Mandatory, RouteBlinding -> Mandatory))
|
||||
val invoice = createBolt12Invoice(Features.empty)
|
||||
val req = SendPaymentToNode(finalAmount, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
|
@ -331,7 +331,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("forward multi-part blinded payment") { f =>
|
||||
import f._
|
||||
val invoice = createBolt12Invoice(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional, RouteBlinding -> Mandatory))
|
||||
val invoice = createBolt12Invoice(Features(BasicMultiPartPayment -> Optional))
|
||||
val req = SendPaymentToNode(finalAmount, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
|
@ -359,7 +359,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("reject blinded payment when route blinding deactivated", Tag(Tags.DisableRouteBlinding)) { f =>
|
||||
import f._
|
||||
val invoice = createBolt12Invoice(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional, RouteBlinding -> Mandatory))
|
||||
val invoice = createBolt12Invoice(Features(BasicMultiPartPayment -> Optional))
|
||||
val req = SendPaymentToNode(finalAmount, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
|
|
|
@ -35,12 +35,13 @@ import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer, PaymentI
|
|||
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData}
|
||||
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload}
|
||||
import fr.acinq.eclair.wire.protocol._
|
||||
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, UInt64, nodeFee, randomBytes32, randomKey}
|
||||
import fr.acinq.eclair.{BlockHeight, Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, UInt64, nodeFee, randomBytes32, randomKey}
|
||||
import org.scalatest.BeforeAndAfterAll
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.bits.{ByteVector, HexStringSyntax}
|
||||
|
||||
import java.util.UUID
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Success
|
||||
|
||||
/**
|
||||
|
@ -177,10 +178,10 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(relay_b.expiryDelta == channelUpdate_bc.cltvExpiryDelta)
|
||||
assert(payload_b.isInstanceOf[IntermediatePayload.ChannelRelay.Standard])
|
||||
|
||||
val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None)
|
||||
val add_c = UpdateAddHtlc(randomBytes32(), 1, relay_b.amountToForward, relay_b.add.paymentHash, relay_b.outgoingCltv, packet_c, None)
|
||||
val Right(relay_c@ChannelRelayPacket(_, payload_c, packet_d)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional))
|
||||
assert(packet_d.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength)
|
||||
assert(relay_c.amountToForward == amount_cd)
|
||||
assert(relay_c.amountToForward >= amount_cd)
|
||||
assert(relay_c.outgoingCltv == expiry_cd)
|
||||
assert(payload_c.outgoingChannelId == channelUpdate_cd.shortChannelId)
|
||||
assert(relay_c.relayFeeMsat == fee_c)
|
||||
|
@ -188,10 +189,10 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(payload_c.isInstanceOf[IntermediatePayload.ChannelRelay.Blinded])
|
||||
val blinding_d = payload_c.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextBlinding
|
||||
|
||||
val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, Some(blinding_d))
|
||||
val add_d = UpdateAddHtlc(randomBytes32(), 2, relay_c.amountToForward, relay_c.add.paymentHash, relay_c.outgoingCltv, packet_d, Some(blinding_d))
|
||||
val Right(relay_d@ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional))
|
||||
assert(packet_e.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength)
|
||||
assert(relay_d.amountToForward == amount_de)
|
||||
assert(relay_d.amountToForward >= amount_de)
|
||||
assert(relay_d.outgoingCltv == expiry_de)
|
||||
assert(payload_d.outgoingChannelId == channelUpdate_de.shortChannelId)
|
||||
assert(relay_d.relayFeeMsat == fee_d)
|
||||
|
@ -199,11 +200,12 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(payload_d.isInstanceOf[IntermediatePayload.ChannelRelay.Blinded])
|
||||
val blinding_e = payload_d.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextBlinding
|
||||
|
||||
val add_e = UpdateAddHtlc(randomBytes32(), 2, amount_de, paymentHash, expiry_de, packet_e, Some(blinding_e))
|
||||
val add_e = UpdateAddHtlc(randomBytes32(), 2, relay_d.amountToForward, relay_d.add.paymentHash, relay_d.outgoingCltv, packet_e, Some(blinding_e))
|
||||
val Right(FinalPacket(_, payload_e)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional))
|
||||
assert(payload_e.amount == finalAmount)
|
||||
assert(payload_e.totalAmount == finalAmount)
|
||||
assert(payload_e.expiry == finalExpiry)
|
||||
assert(add_e.cltvExpiry == finalExpiry)
|
||||
assert(payload_e.expiry == finalExpiry - Channel.MIN_CLTV_EXPIRY_DELTA) // the expiry in the onion doesn't take the min_final_expiry_delta into account
|
||||
assert(payload_e.isInstanceOf[FinalPayload.Blinded])
|
||||
assert(payload_e.asInstanceOf[FinalPayload.Blinded].pathId == hex"deadbeef")
|
||||
}
|
||||
|
@ -211,12 +213,12 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
test("build outgoing blinded payment for introduction node") {
|
||||
// a -> b -> c where c uses a 0-hop blinded route.
|
||||
val recipientKey = randomKey()
|
||||
val features = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional, RouteBlinding -> Mandatory)
|
||||
val features = Features[Bolt12Feature](BasicMultiPartPayment -> Optional)
|
||||
val offer = Offer(None, "Bolt12 r0cks", recipientKey.publicKey, features, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceRequest = InvoiceRequest(offer, amount_bc, 1, features, randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
val blindedRoute = BlindedRouteCreation.createBlindedRouteWithoutHops(c, hex"deadbeef", 1 msat, CltvExpiry(500_000)).route
|
||||
val paymentInfo = PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 1 msat, amount_bc, Features.empty)
|
||||
val invoice = Bolt12Invoice(offer, invoiceRequest, paymentPreimage, recipientKey, CltvExpiryDelta(6), features, Seq(PaymentBlindedRoute(blindedRoute, paymentInfo)))
|
||||
val invoice = Bolt12Invoice(invoiceRequest, paymentPreimage, recipientKey, 300 seconds, features, Seq(PaymentBlindedRoute(blindedRoute, paymentInfo)))
|
||||
val recipient = BlindedRecipient(invoice, amount_bc, expiry_bc, Nil)
|
||||
val hops = Seq(channelHopFromUpdate(a, b, channelUpdate_ab), channelHopFromUpdate(b, c, channelUpdate_bc))
|
||||
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, priv_a.privateKey, Upstream.Local(UUID.randomUUID()), paymentHash, Route(amount_bc, hops, Some(recipient.blindedHops.head)), recipient)
|
||||
|
@ -258,7 +260,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
val Right(FinalPacket(_, payload_b)) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional))
|
||||
assert(payload_b.amount == finalAmount)
|
||||
assert(payload_b.totalAmount == finalAmount)
|
||||
assert(payload_b.expiry == finalExpiry)
|
||||
assert(add_b.cltvExpiry == finalExpiry)
|
||||
assert(payload_b.isInstanceOf[FinalPayload.Blinded])
|
||||
assert(payload_b.asInstanceOf[FinalPayload.Blinded].pathId == hex"123456")
|
||||
}
|
||||
|
@ -272,7 +274,6 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
val Right(FinalPacket(_, payload_b)) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional))
|
||||
assert(payload_b.amount == finalAmount)
|
||||
assert(payload_b.totalAmount == finalAmount)
|
||||
assert(payload_b.expiry == finalExpiry)
|
||||
}
|
||||
|
||||
test("build outgoing trampoline payment") {
|
||||
|
@ -280,7 +281,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
// .----.
|
||||
// / \
|
||||
// a -> b -> c e
|
||||
val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures, paymentMetadata = Some(hex"010203"))
|
||||
val recipient = ClearTrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
|
||||
assert(recipient.trampolineAmount == amount_bc)
|
||||
|
@ -333,7 +334,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
// / \
|
||||
// a -> b -> c e
|
||||
val routingHints = List(List(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144))))
|
||||
val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)
|
||||
val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)
|
||||
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_e.privateKey, Left("#reckless"), CltvExpiryDelta(18), extraHops = routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203"))
|
||||
val recipient = ClearTrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
|
||||
assert(recipient.trampolineAmount == amount_bc)
|
||||
|
@ -388,7 +389,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
|
||||
test("fail to build outgoing trampoline payment when too much payment metadata is provided") {
|
||||
val paymentMetadata = ByteVector.fromValidHex("01" * 400)
|
||||
val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_e.privateKey, Left("Much payment very metadata"), CltvExpiryDelta(9), features = invoiceFeatures, paymentMetadata = Some(paymentMetadata))
|
||||
val recipient = ClearTrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
|
||||
val Left(failure) = buildOutgoingPayment(ActorRef.noSender, priv_a.privateKey, Upstream.Local(UUID.randomUUID()), paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
|
||||
|
@ -403,7 +404,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
}
|
||||
|
||||
test("fail to build outgoing trampoline payment with invalid route") {
|
||||
val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures)
|
||||
val recipient = ClearTrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
|
||||
val route = Route(finalAmount, trampolineChannelHops, None) // missing trampoline hop
|
||||
|
@ -428,7 +429,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
}
|
||||
|
||||
test("fail to decrypt when the trampoline onion is invalid") {
|
||||
val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures, paymentMetadata = Some(hex"010203"))
|
||||
val recipient = ClearTrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
|
||||
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, priv_a.privateKey, Upstream.Local(UUID.randomUUID()), paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
|
||||
|
@ -461,14 +462,14 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
|
||||
test("fail to decrypt when blinded route data is invalid") {
|
||||
val (route, recipient) = {
|
||||
val features = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional, RouteBlinding -> Mandatory)
|
||||
val features = Features[Bolt12Feature](BasicMultiPartPayment -> Optional)
|
||||
val offer = Offer(None, "Bolt12 r0cks", c, features, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceRequest = InvoiceRequest(offer, amount_bc, 1, features, randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
// We send the wrong blinded payload to the introduction node.
|
||||
val tmpBlindedRoute = BlindedRouteCreation.createBlindedRouteFromHops(Seq(channelHopFromUpdate(b, c, channelUpdate_bc)), hex"deadbeef", 1 msat, CltvExpiry(500_000)).route
|
||||
val blindedRoute = tmpBlindedRoute.copy(blindedNodes = tmpBlindedRoute.blindedNodes.reverse)
|
||||
val paymentInfo = OfferTypes.PaymentInfo(fee_b, 0, channelUpdate_bc.cltvExpiryDelta, 0 msat, amount_bc, Features.empty)
|
||||
val invoice = Bolt12Invoice(offer, invoiceRequest, paymentPreimage, priv_c.privateKey, CltvExpiryDelta(6), features, Seq(PaymentBlindedRoute(blindedRoute, paymentInfo)))
|
||||
val invoice = Bolt12Invoice(invoiceRequest, paymentPreimage, priv_c.privateKey, 300 seconds, features, Seq(PaymentBlindedRoute(blindedRoute, paymentInfo)))
|
||||
val recipient = BlindedRecipient(invoice, amount_bc, expiry_bc, Nil)
|
||||
val route = Route(amount_bc, Seq(channelHopFromUpdate(a, b, channelUpdate_ab)), Some(recipient.blindedHops.head))
|
||||
(route, recipient)
|
||||
|
@ -535,10 +536,12 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(payment.cmd.cltvExpiry == expiry_cd)
|
||||
|
||||
// A smaller expiry is sent to d, who doesn't know that it's invalid.
|
||||
val add_d = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, paymentHash, expiry_de, payment.cmd.onion, payment.cmd.nextBlindingKey_opt)
|
||||
// Intermediate nodes can reduce the expiry by at most min_final_expiry_delta.
|
||||
val invalidExpiry = payment.cmd.cltvExpiry - Channel.MIN_CLTV_EXPIRY_DELTA - CltvExpiryDelta(1)
|
||||
val add_d = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, paymentHash, invalidExpiry, payment.cmd.onion, payment.cmd.nextBlindingKey_opt)
|
||||
val Right(relay_d@ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional))
|
||||
assert(payload_d.outgoingChannelId == channelUpdate_de.shortChannelId)
|
||||
assert(relay_d.outgoingCltv < expiry_de)
|
||||
assert(relay_d.outgoingCltv < CltvExpiry(currentBlockCount))
|
||||
assert(payload_d.isInstanceOf[IntermediatePayload.ChannelRelay.Blinded])
|
||||
val blinding_e = payload_d.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextBlinding
|
||||
|
||||
|
@ -567,7 +570,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
//
|
||||
// and return the HTLC sent by b to c.
|
||||
def createIntermediateTrampolinePayment(): UpdateAddHtlc = {
|
||||
val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures)
|
||||
val recipient = ClearTrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
|
||||
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, priv_a.privateKey, Upstream.Local(UUID.randomUUID()), paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
|
||||
|
@ -750,13 +753,13 @@ object PaymentPacketSpec {
|
|||
|
||||
// fully blinded route a -> b
|
||||
def singleBlindedHop(pathId: ByteVector = hex"deadbeef", routeExpiry: CltvExpiry = CltvExpiry(500_000)): (Route, BlindedRecipient) = {
|
||||
val (_, blindedHop, recipient) = blindedRouteFromHops(finalAmount, finalExpiry, Seq(channelHopFromUpdate(a, b, channelUpdate_ab)), routeExpiry, paymentPreimage, pathId)
|
||||
val (_, blindedHop, recipient) = blindedRouteFromHops(finalAmount, CltvExpiry(currentBlockCount), Seq(channelHopFromUpdate(a, b, channelUpdate_ab)), routeExpiry, paymentPreimage, pathId)
|
||||
(Route(finalAmount, Nil, Some(blindedHop)), recipient)
|
||||
}
|
||||
|
||||
// route c -> d -> e, blinded after d
|
||||
def shortBlindedHops(routeExpiry: CltvExpiry = CltvExpiry(500_000)): (Route, BlindedRecipient) = {
|
||||
val (_, blindedHop, recipient) = blindedRouteFromHops(finalAmount, finalExpiry, Seq(channelHopFromUpdate(d, e, channelUpdate_de)), routeExpiry, paymentPreimage)
|
||||
val (_, blindedHop, recipient) = blindedRouteFromHops(finalAmount, CltvExpiry(currentBlockCount), Seq(channelHopFromUpdate(d, e, channelUpdate_de)), routeExpiry, paymentPreimage)
|
||||
(Route(finalAmount, Seq(channelHopFromUpdate(c, d, channelUpdate_cd)), Some(blindedHop)), recipient)
|
||||
}
|
||||
|
||||
|
@ -766,7 +769,7 @@ object PaymentPacketSpec {
|
|||
channelHopFromUpdate(c, d, channelUpdate_cd),
|
||||
channelHopFromUpdate(d, e, channelUpdate_de),
|
||||
)
|
||||
val (invoice, blindedHop, recipient) = blindedRouteFromHops(finalAmount, finalExpiry, hopsToBlind, CltvExpiry(500_000), paymentPreimage, pathId)
|
||||
val (invoice, blindedHop, recipient) = blindedRouteFromHops(finalAmount, CltvExpiry(currentBlockCount), hopsToBlind, CltvExpiry(500_000), paymentPreimage, pathId)
|
||||
val hops = Seq(
|
||||
channelHopFromUpdate(a, b, channelUpdate_ab),
|
||||
channelHopFromUpdate(b, c, channelUpdate_bc),
|
||||
|
|
|
@ -44,7 +44,7 @@ import fr.acinq.eclair.router.Router.RouteRequest
|
|||
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
|
||||
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload}
|
||||
import fr.acinq.eclair.wire.protocol._
|
||||
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey}
|
||||
import fr.acinq.eclair.{BlockHeight, Bolt11Feature, CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey}
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import org.scalatest.{Outcome, Tag}
|
||||
import scodec.bits.HexStringSyntax
|
||||
|
@ -719,7 +719,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
|||
|
||||
// Receive an upstream multi-part payment.
|
||||
val hints = List(ExtraHop(randomKey().publicKey, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))
|
||||
val features = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)
|
||||
val features = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)
|
||||
val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, outgoingNodeKey, Left("Some invoice"), CltvExpiryDelta(18), extraHops = List(hints), paymentMetadata = Some(hex"123456"), features = features)
|
||||
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = IntermediatePayload.NodeRelay.Standard.createNodeRelayToNonTrampolinePayload(
|
||||
incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, invoice
|
||||
|
|
|
@ -195,7 +195,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
|
|||
import f._
|
||||
|
||||
// we use this to build a valid trampoline onion inside a normal onion
|
||||
val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
|
||||
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_c.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures)
|
||||
val trampolineHop = NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b)
|
||||
val recipient = ClearTrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
|
||||
|
|
|
@ -26,6 +26,7 @@ import fr.acinq.eclair.TestConstants.Alice
|
|||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.channel.fsm.Channel
|
||||
import fr.acinq.eclair.crypto.TransportHandler
|
||||
import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager}
|
||||
import fr.acinq.eclair.io.Peer.PeerRoutingMessage
|
||||
|
@ -263,19 +264,17 @@ object BaseRouterSpec {
|
|||
preimage: ByteVector32 = randomBytes32(),
|
||||
pathId: ByteVector = randomBytes(32)): (Bolt12Invoice, BlindedRecipient) = {
|
||||
val recipientKey = randomKey()
|
||||
val features = Features[InvoiceFeature](
|
||||
Features.VariableLengthOnion -> FeatureSupport.Mandatory,
|
||||
val features = Features[Bolt12Feature](
|
||||
Features.BasicMultiPartPayment -> FeatureSupport.Optional,
|
||||
Features.RouteBlinding -> FeatureSupport.Mandatory
|
||||
)
|
||||
val offer = Offer(None, "Bolt12 r0cks", recipientKey.publicKey, features, Block.RegtestGenesisBlock.hash)
|
||||
val invoiceRequest = InvoiceRequest(offer, amount, 1, features, randomKey(), Block.RegtestGenesisBlock.hash)
|
||||
val blindedRoutes = paths.map(hops => {
|
||||
val blindedRoute = BlindedRouteCreation.createBlindedRouteFromHops(hops, pathId, 1 msat, routeExpiry).route
|
||||
val paymentInfo = BlindedRouteCreation.aggregatePaymentInfo(amount, hops)
|
||||
val paymentInfo = BlindedRouteCreation.aggregatePaymentInfo(amount, hops, Channel.MIN_CLTV_EXPIRY_DELTA)
|
||||
PaymentBlindedRoute(blindedRoute, paymentInfo)
|
||||
})
|
||||
val invoice = Bolt12Invoice(offer, invoiceRequest, preimage, recipientKey, CltvExpiryDelta(6), features, blindedRoutes)
|
||||
val invoice = Bolt12Invoice(invoiceRequest, preimage, recipientKey, 300 seconds, features, blindedRoutes)
|
||||
val recipient = BlindedRecipient(invoice, amount, expiry, Nil)
|
||||
(invoice, recipient)
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ class BlindedRouteCreationSpec extends AnyFunSuite with ParallelTestExecution {
|
|||
})
|
||||
for (_ <- 0 to 100) {
|
||||
val amount = rand.nextLong(10_000_000_000L).msat
|
||||
val payInfo = aggregatePaymentInfo(amount, hops)
|
||||
val payInfo = aggregatePaymentInfo(amount, hops, CltvExpiryDelta(0))
|
||||
assert(payInfo.cltvExpiryDelta == CltvExpiryDelta(hops.map(_.cltvExpiryDelta.toInt).sum))
|
||||
// We verify that the aggregated fee slightly exceeds the actual fee (because of proportional fees rounding).
|
||||
val aggregatedFee = payInfo.fee(amount)
|
||||
|
|
|
@ -16,30 +16,21 @@
|
|||
|
||||
package fr.acinq.eclair.wire.protocol
|
||||
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.Bech32
|
||||
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.Features.BasicMultiPartPayment
|
||||
import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceRequestTlvCodec, offerTlvCodec}
|
||||
import fr.acinq.eclair.wire.protocol.OfferTypes._
|
||||
import fr.acinq.eclair.{Features, MilliSatoshiLong, randomBytes32, randomKey}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.bits.{ByteVector, HexStringSyntax}
|
||||
|
||||
import scala.util.Success
|
||||
|
||||
class OfferTypesSpec extends AnyFunSuite {
|
||||
val nodeKey = PrivateKey(hex"85d08273493e489b9330c85a3e54123874c8cd67c1bf531f4b926c9c555f8e1d")
|
||||
val nodeId = nodeKey.publicKey
|
||||
|
||||
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)
|
||||
|
@ -48,74 +39,40 @@ class OfferTypesSpec extends AnyFunSuite {
|
|||
assert(request.checkSignature())
|
||||
}
|
||||
|
||||
test("basic offer") {
|
||||
val offer = Offer(TlvStream[OfferTlv](
|
||||
Description("basic offer"),
|
||||
NodeId(nodeId)))
|
||||
val encoded = "lno1pg9kyctnd93jqmmxvejhy83pqvxl9c6mjgkeaxa6a0vtxqteql688v0ywa8qqwx4j05cyskn8ncrj"
|
||||
test("minimal offer") {
|
||||
val tlvs = Seq(
|
||||
OfferDescription("basic offer"),
|
||||
OfferNodeId(nodeId))
|
||||
val offer = Offer(TlvStream(tlvs))
|
||||
val encoded = "lno1pg9kyctnd93jqmmxvejhy93pqvxl9c6mjgkeaxa6a0vtxqteql688v0ywa8qqwx4j05cyskn8ncrj"
|
||||
assert(Offer.decode(encoded).get == offer)
|
||||
assert(offer.amount.isEmpty)
|
||||
assert(offer.signature.isEmpty)
|
||||
assert(offer.description == "basic offer")
|
||||
assert(offer.nodeId == nodeId)
|
||||
}
|
||||
|
||||
test("basic signed offer") {
|
||||
val signedOffer = Offer(TlvStream[OfferTlv](
|
||||
Description("basic signed offer"),
|
||||
NodeId(nodeId))).sign(nodeKey)
|
||||
val encoded = "lno1pgfxyctnd93jqumfvahx2epqdanxvetjrcssxr0juddeytv7nwawhk9nq9us0arnk8j8wnsq8r2e86vzgtfneupe7pqr8k27dajwvrehuprj08ggamld6pgnj9ydp3whx6kz5hjaavh8rhfjzkhyxsjwakelepqxmx26aqhlaslxn8ljn4mtm2cx76xz72kxkc"
|
||||
assert(Offer.decode(encoded).get == signedOffer)
|
||||
assert(signedOffer.checkSignature())
|
||||
assert(signedOffer.amount.isEmpty)
|
||||
assert(signedOffer.description == "basic signed offer")
|
||||
assert(signedOffer.nodeId == nodeId)
|
||||
// Removing any TLV from the minimal offer makes it invalid.
|
||||
for (tlv <- tlvs) {
|
||||
val incomplete = TlvStream[OfferTlv](tlvs.filterNot(_ == tlv))
|
||||
assert(Offer.validate(incomplete).isLeft)
|
||||
val incompleteEncoded = Bech32.encodeBytes(Offer.hrp, offerTlvCodec.encode(incomplete).require.bytes.toArray, Bech32.Encoding.Beck32WithoutChecksum)
|
||||
assert(Offer.decode(incompleteEncoded).isFailure)
|
||||
}
|
||||
}
|
||||
|
||||
test("offer with amount and quantity") {
|
||||
val offer = Offer(TlvStream[OfferTlv](
|
||||
Chains(Seq(Block.TestnetGenesisBlock.hash)),
|
||||
Amount(50 msat),
|
||||
Description("offer with quantity"),
|
||||
Issuer("alice@bigshop.com"),
|
||||
QuantityMin(1),
|
||||
NodeId(nodeKey.publicKey)))
|
||||
val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968j9q3v9kxjcm9gp3xjemndphhqtnrdak3vqgprcssxr0juddeytv7nwawhk9nq9us0arnk8j8wnsq8r2e86vzgtfneupe"
|
||||
OfferChains(Seq(Block.TestnetGenesisBlock.hash)),
|
||||
OfferAmount(50 msat),
|
||||
OfferDescription("offer with quantity"),
|
||||
OfferIssuer("alice@bigshop.com"),
|
||||
OfferQuantityMax(0),
|
||||
OfferNodeId(nodeKey.publicKey)))
|
||||
val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968jys3v9kxjcm9gp3xjemndphhqtnrdak3gqqkyypsmuhrtwfzm85mht4a3vcp0yrlgua3u3m5uqpc6kf7nqjz6v70qwg"
|
||||
assert(Offer.decode(encoded).get == offer)
|
||||
assert(offer.amount.contains(50 msat))
|
||||
assert(offer.signature.isEmpty)
|
||||
assert(offer.description == "offer with quantity")
|
||||
assert(offer.nodeId == nodeId)
|
||||
assert(offer.issuer.contains("alice@bigshop.com"))
|
||||
assert(offer.quantityMin.contains(1))
|
||||
}
|
||||
|
||||
test("signed offer with amount and quantity") {
|
||||
val signedOffer = Offer(TlvStream[OfferTlv](
|
||||
Chains(Seq(Block.TestnetGenesisBlock.hash)),
|
||||
Amount(50 msat),
|
||||
Description("offer with quantity"),
|
||||
Issuer("alice@bigshop.com"),
|
||||
QuantityMin(1),
|
||||
NodeId(nodeKey.publicKey))).sign(nodeKey)
|
||||
val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968j9q3v9kxjcm9gp3xjemndphhqtnrdak3vqgprcssxr0juddeytv7nwawhk9nq9us0arnk8j8wnsq8r2e86vzgtfneupe7pqte5yn8m6mtk5racz9c0hgw6smxhp5t0ns77huttmy4632f6t2ns3jdwkxh9qy9f2eun2329gswcz38dn5f2us4us2r76zxvhkj9uecv"
|
||||
assert(Offer.decode(encoded).get == signedOffer)
|
||||
assert(signedOffer.checkSignature())
|
||||
assert(signedOffer.amount.contains(50 msat))
|
||||
assert(signedOffer.description == "offer with quantity")
|
||||
assert(signedOffer.nodeId == nodeId)
|
||||
assert(signedOffer.issuer.contains("alice@bigshop.com"))
|
||||
assert(signedOffer.quantityMin.contains(1))
|
||||
}
|
||||
|
||||
test("decode invalid offer") {
|
||||
val testCases = Seq(
|
||||
"lno1pgxx7enxv4e8xgrjda3kkgg", // missing node id
|
||||
"lno1rcsdhss957tylk58rmly849jupnmzs52ydhxhl8fgz7994xkf2hnwhg", // missing description
|
||||
)
|
||||
for (testCase <- testCases) {
|
||||
assert(Offer.decode(testCase).isFailure)
|
||||
}
|
||||
assert(offer.quantityMax.contains(Long.MaxValue))
|
||||
}
|
||||
|
||||
def signInvoiceRequest(request: InvoiceRequest, key: PrivateKey): InvoiceRequest = {
|
||||
|
@ -131,24 +88,22 @@ class OfferTypesSpec extends AnyFunSuite {
|
|||
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)
|
||||
val biggerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case InvoiceRequestAmount(_) => InvoiceRequestAmount(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)
|
||||
val lowerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case InvoiceRequestAmount(_) => InvoiceRequestAmount(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)
|
||||
val withQuantity = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(InvoiceRequestQuantity(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 offer = Offer(Some(2500 msat), "offer with features", randomKey().publicKey, Features.empty, Block.LivenetGenesisBlock.hash)
|
||||
val payerKey = randomKey()
|
||||
val request = InvoiceRequest(offer, 2500 msat, 1, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional), payerKey, Block.LivenetGenesisBlock.hash)
|
||||
val request = InvoiceRequest(offer, 2500 msat, 1, Features(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)
|
||||
val otherFeatures = InvoiceRequest(offer, 2500 msat, 1, Features(BasicMultiPartPayment -> Mandatory), payerKey, Block.LivenetGenesisBlock.hash)
|
||||
assert(!otherFeatures.isValidFor(offer))
|
||||
}
|
||||
|
||||
|
@ -157,91 +112,85 @@ class OfferTypesSpec extends AnyFunSuite {
|
|||
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)
|
||||
val withoutAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.filter { case InvoiceRequestAmount(_) => 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 offer = Offer(TlvStream(Seq(OfferAmount(100 msat), OfferDescription("offer without chains"), OfferNodeId(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 tlvs: Seq[InvoiceRequestTlv] = (offer.records.records ++ Seq(
|
||||
InvoiceRequestMetadata(hex"012345"),
|
||||
InvoiceRequestAmount(100 msat),
|
||||
InvoiceRequestPayerId(payerKey.publicKey),
|
||||
)).toSeq
|
||||
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)
|
||||
val withDefaultChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(InvoiceRequestChain(Block.LivenetGenesisBlock.hash)))), payerKey)
|
||||
assert(withDefaultChain.isValidFor(offer))
|
||||
val otherChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Chain(Block.TestnetGenesisBlock.hash)))), payerKey)
|
||||
val otherChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(InvoiceRequestChain(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 offer = Offer(TlvStream(Seq(OfferChains(Seq(chain1, chain2)), OfferAmount(100 msat), OfferDescription("offer with chains"), OfferNodeId(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)
|
||||
val noChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.filter { case InvoiceRequestChain(_) => 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)
|
||||
val otherChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.map { case InvoiceRequestChain(_) => InvoiceRequestChain(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),
|
||||
OfferAmount(500 msat),
|
||||
OfferDescription("offer for multiple items"),
|
||||
OfferNodeId(randomKey().publicKey),
|
||||
OfferQuantityMax(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.records.get[InvoiceRequestQuantity].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",
|
||||
test("minimal invoice request") {
|
||||
val payerKey = PrivateKey(hex"527d410ec920b626ece685e8af9abc976a48dbf2fe698c1b35d90a1c5fa2fbca")
|
||||
val tlvsWithoutSignature = Seq(
|
||||
InvoiceRequestMetadata(hex"abcdef"),
|
||||
OfferDescription("basic offer"),
|
||||
OfferNodeId(nodeId),
|
||||
InvoiceRequestPayerId(payerKey.publicKey),
|
||||
)
|
||||
for (testCase <- testCases) {
|
||||
assert(InvoiceRequest.decode(testCase).isFailure)
|
||||
val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream[InvoiceRequestTlv](tlvsWithoutSignature), OfferCodecs.invoiceRequestTlvCodec), payerKey)
|
||||
val tlvs = tlvsWithoutSignature :+ Signature(signature)
|
||||
val invoiceRequest = InvoiceRequest(TlvStream(tlvs))
|
||||
val encoded = "lnr1qqp6hn00pg9kyctnd93jqmmxvejhy93pqvxl9c6mjgkeaxa6a0vtxqteql688v0ywa8qqwx4j05cyskn8ncrjkppqfxajawru7sa7rt300hfzs2lyk2jrxduxrkx9lmzy6lxcvfhk0j7ruzqc4mtjj5fwukrqp7faqrxn664nmwykad76pu997terewcklsx47apag59wf8exly4tky7y63prr7450n28stqssmzuf48w7e6rjad2eq"
|
||||
assert(InvoiceRequest.decode(encoded).get == invoiceRequest)
|
||||
assert(invoiceRequest.offer.amount.isEmpty)
|
||||
assert(invoiceRequest.offer.description == "basic offer")
|
||||
assert(invoiceRequest.offer.nodeId == nodeId)
|
||||
assert(invoiceRequest.metadata == hex"abcdef")
|
||||
assert(invoiceRequest.payerId == payerKey.publicKey)
|
||||
// Removing any TLV from the minimal invoice request makes it invalid.
|
||||
for (tlv <- tlvs) {
|
||||
val incomplete = TlvStream[InvoiceRequestTlv](tlvs.filterNot(_ == tlv))
|
||||
assert(InvoiceRequest.validate(incomplete).isLeft)
|
||||
val incompleteEncoded = Bech32.encodeBytes(InvoiceRequest.hrp, invoiceRequestTlvCodec.encode(incomplete).require.bytes.toArray, Bech32.Encoding.Beck32WithoutChecksum)
|
||||
assert(InvoiceRequest.decode(incompleteEncoded).isFailure)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,43 +202,43 @@ class OfferTypesSpec extends AnyFunSuite {
|
|||
|
||||
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")),
|
||||
TestCase(hex"010203e8", 1, ByteVector32(hex"b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93")),
|
||||
TestCase(hex"010203e8 02080000010000020003", 2, ByteVector32(hex"c3774abbf4815aa54ccaa026bff6581f01f3be5fe814c620a252534f434bc0d1")),
|
||||
TestCase(hex"010203e8 02080000010000020003 03310266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c0351800000000000000010000000000000002", 3, ByteVector32(hex"ab2e79b1283b0b31e0b035258de23782df6b89a38cfa7237bde69aed1a658c5d")),
|
||||
TestCase(hex"0008000000000000000006035553440801640a1741204d617468656d61746963616c205472656174697365162102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661958210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", 6, ByteVector32(hex"608407c18ad9a94d9ea2bcdbe170b6c20c462a7833a197621c916f78cf18e624")),
|
||||
// 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")),
|
||||
TestCase(hex"010100", 1, ByteVector32(hex"14ffa5e1e5d861059abff167dad6e632c45483006f7d4dc4355586062a3da30d")),
|
||||
TestCase(hex"010100 020100", 2, ByteVector32(hex"ec0584e764b71cb49ebe60ce7edbab8387e42da20b6077031bd27ff345b38ff8")),
|
||||
TestCase(hex"010100 020100 030100", 3, ByteVector32(hex"cc68aea3dc863832ef6828b3da8689cce3478c934cc50a68522477506a35feb2")),
|
||||
TestCase(hex"010100 020100 030100 040100", 4, ByteVector32(hex"b531eaa1ca71956148a6756cf8f46bdf231879e6c392019877f23e56acb7b956")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100", 5, ByteVector32(hex"104e383bfdcb620cd8cefa95245332e8bd32ffd8d974fffdafe1488b1f4a1fbd")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100", 6, ByteVector32(hex"d96f0769702cb3440abbe683d7211fd20bd152699352f09f45d2695a89d18cdc")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100", 7, ByteVector32(hex"30b8886e306c97dbc7b730a2e99138c1ea4fdf5c2f71e2a31e434f63f5eed228")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100", 8, ByteVector32(hex"783262efe5eeef4ec96bcee8d7cf5149ea44e0c28a78f4b1cb73d6cec9a0b378")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100", 9, ByteVector32(hex"6fd20b65a0097aff2bcc70753612a296edc27933ea335bac5df2e4c724cdb43c")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100", 10, ByteVector32(hex"9a3cf7785e9c84e03d6bc7fc04226a1cb19f158a69f16684663aa710bd90a14b")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100", 11, ByteVector32(hex"ace50a04d9dc82ce123c6ac6c2449fa607054560a9a7b8229cd2d47c01b94953")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100", 12, ByteVector32(hex"1a8e85042447a10ec312b35db34d0c8722caba4aaf6a170c4506d1fdb520aa66")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100", 13, ByteVector32(hex"8c3b8d9ba90eb9a4a34c890a7a24ba6ddc873529c5fd7c95f33a5b9ba589f54b")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100", 14, ByteVector32(hex"ed9e3694bbad2fca636576cc69af4c63ad64023bfeb788fe0f40b3533b248a6a")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100", 15, ByteVector32(hex"bab201e05786ae1eae4d685b4f815134158720ba297ea0f46a9420ffe5e94b16")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100", 16, ByteVector32(hex"44438261bb64672f374d8782e92dc9616e900378ce4bd64442753722bc2a1acb")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100", 17, ByteVector32(hex"bb6fbcd5cf426ec0b7e49d9f9ccc6c15319e01f007cce8f16fa802016718b9f7")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100", 18, ByteVector32(hex"64d8639e76af096223cad2c448d68fabf751d1c6a939bc86e1015b19188202dc")),
|
||||
TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100", 19, ByteVector32(hex"bcb88f8e06886a6d422d14bc2ed4e7fc06c0ad2adeedf630a73972c5b15538ca")),
|
||||
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"9deddd5f0ab909e6a161fd4b9d44ed7384ee0a7fe8d3fbb637872767eab82f1e")),
|
||||
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"4a32a2325bbd1c2b5b4915c6bec6b3e3d734d956e0c123f1fa6d70f7a8609dcd")),
|
||||
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"a3ec28f0f9cb64db8d96dd7b9039fbf2240438401ea992df802d7bb70b3d02af")),
|
||||
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"d025f268ec4f09baf51c4b94287e76707d9353e8cab31dc586ae47742ba0b266")),
|
||||
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"cd5a2086a3919d67d0617da1e6e293f115bed8d8306498ed814c6c109ad370a4")),
|
||||
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"f64113810b52f4d6a55380a3d84e59e34d26c145448121c2113a023cb63de71b")),
|
||||
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"b99d7332ea2db048093a7bc0aaa85f82ccfa9da2b734fc0a14b79c5dac5a3a1c")),
|
||||
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"fab01a3ce6e878942dc5c9c862cb18e88202d50e6026d2266748f7eda5f9db7f")),
|
||||
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"2dc8b24a0e142d1ed36a144ed35ef0d4b7d0d1b51e198b2282248e45ebaf0417")),
|
||||
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"3693a858cc97762d69d05b2191d3e5254c29ddb5abac5b9fe52b227fa216aa4c")),
|
||||
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"db8787d4509265e764e60b7a81cf38efb9d3a7910d67c4ae68a1232436e1cd3b")),
|
||||
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"af49f35e5b2565cb229f342405783d330c56031f005a4a6ca01f87e5637d4614")),
|
||||
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"2e9f8a8542576197650f61c882625f0f6838f962f9fa24ce809b687784a8a7de")),
|
||||
)
|
||||
testCases.foreach {
|
||||
case TestCase(tlvStream, tlvCount, expectedRoot) =>
|
||||
|
@ -297,7 +246,7 @@ class OfferTypesSpec extends AnyFunSuite {
|
|||
val tlvs = genericTlvStream.decode(tlvStream.bits).require.value
|
||||
assert(tlvs.records.size == tlvCount)
|
||||
val root = OfferTypes.rootHash(tlvs, genericTlvStream)
|
||||
assert(root == expectedRoot)
|
||||
assert(root == expectedRoot)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -201,8 +201,8 @@ class PaymentOnionSpec extends AnyFunSuite {
|
|||
RouteBlindingEncryptedDataTlv.PaymentConstraints(CltvExpiry(1500), 1 msat),
|
||||
)
|
||||
val testCases = Map(
|
||||
TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), EncryptedRecipientData(hex"deadbeef"), TotalAmount(1105 msat)) -> hex"11 02020231 04012a 0a04deadbeef 12020451",
|
||||
TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), EncryptedRecipientData(hex"deadbeef"), BlindingPoint(PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2")), TotalAmount(1105 msat)) -> hex"34 02020231 04012a 0a04deadbeef 0c21036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2 12020451",
|
||||
TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(1234567)), EncryptedRecipientData(hex"deadbeef"), TotalAmount(1105 msat)) -> hex"13 02020231 040312d687 0a04deadbeef 12020451",
|
||||
TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(1234567)), EncryptedRecipientData(hex"deadbeef"), BlindingPoint(PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2")), TotalAmount(1105 msat)) -> hex"36 02020231 040312d687 0a04deadbeef 0c21036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2 12020451",
|
||||
)
|
||||
|
||||
for ((expected, bin) <- testCases) {
|
||||
|
@ -211,7 +211,7 @@ class PaymentOnionSpec extends AnyFunSuite {
|
|||
val Right(payload) = FinalPayload.Blinded.validate(decoded, blindedTlvs)
|
||||
assert(payload.amount == 561.msat)
|
||||
assert(payload.totalAmount == 1105.msat)
|
||||
assert(payload.expiry == CltvExpiry(42))
|
||||
assert(payload.expiry == CltvExpiry(1234567))
|
||||
assert(payload.pathId == hex"2a2a2a2a")
|
||||
val encoded = perHopPayloadCodec.encode(expected).require.bytes
|
||||
assert(encoded == bin)
|
||||
|
|
Loading…
Add table
Reference in a new issue