From d488743d089ba7f9779df469b1f0840cd3c32505 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 13 Feb 2024 14:32:19 +0100 Subject: [PATCH] Bolt12Invoice --- .../kotlin/fr/acinq/lightning/Features.kt | 13 +- .../fr/acinq/lightning/db/PaymentsDb.kt | 7 +- .../kotlin/fr/acinq/lightning/io/Peer.kt | 6 +- .../acinq/lightning/payment/Bolt11Invoice.kt | 24 +- .../acinq/lightning/payment/Bolt12Invoice.kt | 160 ++++++ .../payment/IncomingPaymentHandler.kt | 12 +- .../payment/OutgoingPaymentHandler.kt | 4 +- .../payment/OutgoingPaymentPacket.kt | 2 +- .../acinq/lightning/payment/PaymentRequest.kt | 14 + .../fr/acinq/lightning/wire/PaymentOnion.kt | 14 +- .../fr/acinq/lightning/wire/RouteBlinding.kt | 80 ++- .../lightning/db/PaymentsDbTestsCommon.kt | 11 +- ...sCommon.kt => Bolt11InvoiceTestsCommon.kt} | 188 +++--- .../payment/Bolt12InvoiceTestsCommon.kt | 543 ++++++++++++++++++ .../OutgoingPaymentHandlerTestsCommon.kt | 66 +-- .../payment/PaymentPacketTestsCommon.kt | 30 +- .../lightning/wire/PaymentOnionTestsCommon.kt | 5 +- 17 files changed, 989 insertions(+), 190 deletions(-) create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/payment/PaymentRequest.kt rename src/commonTest/kotlin/fr/acinq/lightning/payment/{PaymentRequestTestsCommon.kt => Bolt11InvoiceTestsCommon.kt} (82%) create mode 100644 src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt diff --git a/src/commonMain/kotlin/fr/acinq/lightning/Features.kt b/src/commonMain/kotlin/fr/acinq/lightning/Features.kt index 7d2fe24b0..a819b9ecb 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/Features.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/Features.kt @@ -7,7 +7,7 @@ import fr.acinq.lightning.utils.or import kotlinx.serialization.Serializable /** Feature scope as defined in Bolt 9. */ -enum class FeatureScope { Init, Node, Invoice } +enum class FeatureScope { Init, Node, Invoice, Bolt12 } enum class FeatureSupport { Mandatory { @@ -88,7 +88,7 @@ sealed class Feature { object BasicMultiPartPayment : Feature() { override val rfcName get() = "basic_mpp" override val mandatory get() = 16 - override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice) + override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice, FeatureScope.Bolt12) } @Serializable @@ -105,6 +105,13 @@ sealed class Feature { override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) } + @Serializable + object RouteBlinding : Feature() { + override val rfcName get() = "option_route_blinding" + override val mandatory get() = 24 + override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice) + } + @Serializable object ShutdownAnySegwit : Feature() { override val rfcName get() = "option_shutdown_anysegwit" @@ -268,6 +275,8 @@ data class Features(val activated: Map, val unknown: Se fun invoiceFeatures(): Features = Features(activated.filter { it.key.scopes.contains(FeatureScope.Invoice) }, unknown) + fun bolt12Features(): Features = Features(activated.filter { it.key.scopes.contains(FeatureScope.Bolt12) }, unknown) + /** NB: this method is not reflexive, see [[Features.areCompatible]] if you want symmetric validation. */ fun areSupported(remoteFeatures: Features): Boolean { // we allow unknown odd features (it's ok to be odd) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt index 2c705ee19..01bc33eac 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt @@ -5,6 +5,7 @@ import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.channel.ChannelException +import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.payment.FinalFailure import fr.acinq.lightning.payment.PaymentRequest import fr.acinq.lightning.utils.* @@ -147,7 +148,7 @@ data class IncomingPayment(val preimage: ByteVector32, val origin: Origin, val r sealed class Origin { /** A normal, invoice-based lightning payment. */ - data class Invoice(val paymentRequest: PaymentRequest) : Origin() + data class Invoice(val paymentRequest: Bolt11Invoice) : Origin() /** KeySend payments are spontaneous donations for which we didn't create an invoice. */ data object KeySend : Origin() @@ -249,7 +250,7 @@ data class LightningOutgoingPayment( ) : OutgoingPayment() { /** Create an outgoing payment in a pending status, without any parts yet. */ - constructor(id: UUID, amount: MilliSatoshi, recipient: PublicKey, invoice: PaymentRequest) : this(id, amount, recipient, Details.Normal(invoice), listOf(), Status.Pending) + constructor(id: UUID, amount: MilliSatoshi, recipient: PublicKey, invoice: Bolt11Invoice) : this(id, amount, recipient, Details.Normal(invoice), listOf(), Status.Pending) val paymentHash: ByteVector32 = details.paymentHash @@ -284,7 +285,7 @@ data class LightningOutgoingPayment( abstract val paymentHash: ByteVector32 /** A normal lightning payment. */ - data class Normal(val paymentRequest: PaymentRequest) : Details() { + data class Normal(val paymentRequest: Bolt11Invoice) : Details() { override val paymentHash: ByteVector32 = paymentRequest.paymentHash } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index ea21ba563..c6da418a5 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -70,7 +70,7 @@ data object Disconnected : PeerCommand() sealed class PaymentCommand : PeerCommand() private data object CheckPaymentsTimeout : PaymentCommand() data class PayToOpenResponseCommand(val payToOpenResponse: PayToOpenResponse) : PeerCommand() -data class SendPayment(val paymentId: UUID, val amount: MilliSatoshi, val recipient: PublicKey, val paymentRequest: PaymentRequest, val trampolineFeesOverride: List? = null) : PaymentCommand() { +data class SendPayment(val paymentId: UUID, val amount: MilliSatoshi, val recipient: PublicKey, val paymentRequest: Bolt11Invoice, val trampolineFeesOverride: List? = null) : PaymentCommand() { val paymentHash: ByteVector32 = paymentRequest.paymentHash } @@ -614,7 +614,7 @@ class Peer( } } - suspend fun createInvoice(paymentPreimage: ByteVector32, amount: MilliSatoshi?, description: Either, expirySeconds: Long? = null): PaymentRequest { + suspend fun createInvoice(paymentPreimage: ByteVector32, amount: MilliSatoshi?, description: Either, expirySeconds: Long? = null): Bolt11Invoice { // we add one extra hop which uses a virtual channel with a "peer id", using the highest remote fees and expiry across all // channels to maximize the likelihood of success on the first payment attempt val remoteChannelUpdates = _channels.values.mapNotNull { channelState -> @@ -627,7 +627,7 @@ class Peer( } val extraHops = listOf( listOf( - PaymentRequest.TaggedField.ExtraHop( + Bolt11Invoice.TaggedField.ExtraHop( nodeId = walletParams.trampolineNode.id, shortChannelId = ShortChannelId.peerId(nodeParams.nodeId), feeBase = remoteChannelUpdates.maxOfOrNull { it.feeBaseMsat } ?: walletParams.invoiceDefaultRoutingFees.feeBase, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt11Invoice.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt11Invoice.kt index 19bfac249..9fe2d8033 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt11Invoice.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt11Invoice.kt @@ -13,15 +13,15 @@ import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.LightningCodecs import kotlin.experimental.and -data class PaymentRequest( +data class Bolt11Invoice( val prefix: String, - val amount: MilliSatoshi?, + override val amount: MilliSatoshi?, val timestampSeconds: Long, val nodeId: PublicKey, val tags: List, val signature: ByteVector -) { - val paymentHash: ByteVector32 = tags.find { it is TaggedField.PaymentHash }!!.run { (this as TaggedField.PaymentHash).hash } +) : PaymentRequest { + override val paymentHash: ByteVector32 = tags.find { it is TaggedField.PaymentHash }!!.run { (this as TaggedField.PaymentHash).hash } val paymentSecret: ByteVector32 = tags.find { it is TaggedField.PaymentSecret }!!.run { (this as TaggedField.PaymentSecret).secret } @@ -37,12 +37,12 @@ data class PaymentRequest( val fallbackAddress: String? = tags.find { it is TaggedField.FallbackAddress }?.run { (this as TaggedField.FallbackAddress).toAddress(prefix) } - val features: ByteVector = tags.find { it is TaggedField.Features }.run { (this as TaggedField.Features).bits } + override val features: Features = tags.find { it is TaggedField.Features }.run { Features((this as TaggedField.Features).bits) } val routingInfo: List = tags.filterIsInstance() init { - val f = Features(features).invoiceFeatures() + val f = features.invoiceFeatures() require(f.hasFeature(Feature.VariableLengthOnion)) { "${Feature.VariableLengthOnion.rfcName} must be supported" } require(f.hasFeature(Feature.PaymentSecret)) { "${Feature.PaymentSecret.rfcName} must be supported" } require(Features.validateFeatureGraph(f) == null) @@ -53,7 +53,7 @@ data class PaymentRequest( require(description != null || descriptionHash != null) { "there must be exactly one description tag or one description hash tag" } } - fun isExpired(currentTimestampSeconds: Long = currentTimestampSeconds()): Boolean = when (expirySeconds) { + override fun isExpired(currentTimestampSeconds: Long): Boolean = when (expirySeconds) { null -> timestampSeconds + DEFAULT_EXPIRY_SECONDS <= currentTimestampSeconds else -> timestampSeconds + expirySeconds <= currentTimestampSeconds } @@ -91,7 +91,7 @@ data class PaymentRequest( * @param privateKey private key, which must match the payment request's node id * @return a signature (64 bytes) plus a recovery id (1 byte) */ - fun sign(privateKey: PrivateKey): PaymentRequest { + fun sign(privateKey: PrivateKey): Bolt11Invoice { require(privateKey.publicKey() == nodeId) { "private key does not match node id" } val msg = signedHash() val sig = Crypto.sign(msg, privateKey) @@ -143,7 +143,7 @@ data class PaymentRequest( expirySeconds: Long? = null, extraHops: List> = listOf(), timestampSeconds: Long = currentTimestampSeconds() - ): PaymentRequest { + ): Bolt11Invoice { val prefix = prefixes[chainHash] ?: error("unknown chain hash") val tags = mutableListOf( TaggedField.PaymentHash(paymentHash), @@ -160,7 +160,7 @@ data class PaymentRequest( extraHops.forEach { tags.add(TaggedField.RoutingInfo(it)) } } - return PaymentRequest( + return Bolt11Invoice( prefix = prefix, amount = amount, timestampSeconds = timestampSeconds, @@ -177,7 +177,7 @@ data class PaymentRequest( return loop(input, listOf()) } - fun read(input: String): Try = runTrying { + fun read(input: String): Try = runTrying { val (hrp, data) = Bech32.decode(input) val prefix = prefixes.values.find { hrp.startsWith(it) } ?: throw IllegalArgumentException("unknown prefix $hrp") val amount = decodeAmount(hrp.drop(prefix.length)) @@ -219,7 +219,7 @@ data class PaymentRequest( } loop(data.drop(7).dropLast(104)) - val pr = PaymentRequest(prefix, amount, timestamp, nodeId, tags, sigandrecid.toByteVector()) + val pr = Bolt11Invoice(prefix, amount, timestamp, nodeId, tags, sigandrecid.toByteVector()) require(pr.signedPreimage().contentEquals(tohash)) { "invoice isn't canonically encoded" } pr } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt new file mode 100644 index 000000000..e3f4d3efb --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt @@ -0,0 +1,160 @@ +package fr.acinq.lightning.payment + +import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.utils.Either +import fr.acinq.bitcoin.utils.Try +import fr.acinq.bitcoin.utils.runTrying +import fr.acinq.lightning.Feature +import fr.acinq.lightning.FeatureSupport +import fr.acinq.lightning.Features +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.utils.currentTimestampSeconds +import fr.acinq.lightning.wire.GenericTlv +import fr.acinq.lightning.wire.OfferTypes +import fr.acinq.lightning.wire.OfferTypes.ContactInfo.BlindedPath +import fr.acinq.lightning.wire.OfferTypes.FallbackAddress +import fr.acinq.lightning.wire.OfferTypes.InvalidTlvPayload +import fr.acinq.lightning.wire.OfferTypes.InvoiceAmount +import fr.acinq.lightning.wire.OfferTypes.InvoiceBlindedPay +import fr.acinq.lightning.wire.OfferTypes.InvoiceCreatedAt +import fr.acinq.lightning.wire.OfferTypes.InvoiceFallbacks +import fr.acinq.lightning.wire.OfferTypes.InvoiceFeatures +import fr.acinq.lightning.wire.OfferTypes.InvoiceNodeId +import fr.acinq.lightning.wire.OfferTypes.InvoicePaths +import fr.acinq.lightning.wire.OfferTypes.InvoicePaymentHash +import fr.acinq.lightning.wire.OfferTypes.InvoiceRelativeExpiry +import fr.acinq.lightning.wire.OfferTypes.InvoiceRequest +import fr.acinq.lightning.wire.OfferTypes.InvoiceTlv +import fr.acinq.lightning.wire.OfferTypes.MissingRequiredTlv +import fr.acinq.lightning.wire.OfferTypes.PaymentInfo +import fr.acinq.lightning.wire.OfferTypes.Signature +import fr.acinq.lightning.wire.OfferTypes.filterInvoiceRequestFields +import fr.acinq.lightning.wire.OfferTypes.removeSignature +import fr.acinq.lightning.wire.OfferTypes.rootHash +import fr.acinq.lightning.wire.OfferTypes.signSchnorr +import fr.acinq.lightning.wire.OfferTypes.verifySchnorr +import fr.acinq.lightning.wire.TlvStream + +data class Bolt12Invoice(val records: TlvStream) : PaymentRequest { + val invoiceRequest: InvoiceRequest = InvoiceRequest.validate(filterInvoiceRequestFields(records)).right!! + + override val amount: MilliSatoshi? = records.get()?.amount + val nodeId: PublicKey = records.get()!!.nodeId + override val paymentHash: ByteVector32 = records.get()!!.hash + val description: String = invoiceRequest.offer.description + val createdAtSeconds: Long = records.get()!!.timestampSeconds + val relativeExpirySeconds: Long = records.get()?.seconds ?: DEFAULT_EXPIRY_SECONDS + + + // We add invoice features that are implicitly required for Bolt 12 (the spec doesn't allow explicitly setting them). + override val features: Features = + (records.get()?.features?.invoiceFeatures() ?: Features.empty).let { + it.copy(activated = it.activated + (Feature.VariableLengthOnion to FeatureSupport.Mandatory) + (Feature.RouteBlinding to FeatureSupport.Mandatory)) + } + + val blindedPaths: List = records.get()!!.paths.zip(records.get()!!.paymentInfos).map { PaymentBlindedContactInfo(it.first, it.second) } + val fallbacks: List? = records.get()?.addresses + val signature: ByteVector64 = records.get()!!.signature + + + override fun isExpired(currentTimestampSeconds: Long): Boolean = createdAtSeconds + relativeExpirySeconds <= currentTimestampSeconds + + // It is assumed that the request is valid for this offer. + fun validateFor(request: InvoiceRequest): Either = + if (invoiceRequest.unsigned() != request.unsigned()) { + Either.Left("Invoice does not match request") + } else if (nodeId != invoiceRequest.offer.nodeId) { + Either.Left("Wrong node id") + } else if (isExpired()) { + Either.Left("Invoice expired") + } else if (request.amount != null && amount != null && request.amount != amount) { + Either.Left("Incompatible amount") + } else if (!Features.areCompatible(request.features, features.bolt12Features())) { + Either.Left("Incompatible features") + } else if (!checkSignature()) { + Either.Left("Invalid signature") + } else { + Either.Right(Unit) + } + + fun checkSignature(): Boolean = + verifySchnorr(signatureTag, rootHash(removeSignature(records)), signature, nodeId) + + override fun toString(): String { + val data = OfferTypes.Invoice.tlvSerializer.write(records) + return Bech32.encodeBytes(hrp, data, Bech32.Encoding.Beck32WithoutChecksum) + } + + companion object { + val hrp = "lni" + val signatureTag: ByteVector = ByteVector(("lightning" + "invoice" + "signature").encodeToByteArray()) + val DEFAULT_EXPIRY_SECONDS: Long = 7200 + + data class PaymentBlindedContactInfo(val route: BlindedPath, val paymentInfo: PaymentInfo) + + /** + * Creates an invoice for a given offer and invoice request. + * + * @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 + */ + operator fun invoke( + request: InvoiceRequest, + preimage: ByteVector32, + nodeKey: PrivateKey, + invoiceExpirySeconds: Long, + features: Features, + paths: List, + additionalTlvs: Set = setOf(), + customTlvs: Set = setOf() + ): Bolt12Invoice { + require(request.amount != null || request.offer.amount != null) + val amount = request.amount ?: (request.offer.amount!! * request.quantity) + val tlvs: Set = removeSignature(request.records).records + setOfNotNull( + InvoicePaths(paths.map { it.route }), + InvoiceBlindedPay(paths.map { it.paymentInfo }), + InvoiceCreatedAt(currentTimestampSeconds()), + InvoiceRelativeExpiry(invoiceExpirySeconds), + InvoicePaymentHash(ByteVector32(Crypto.sha256(preimage))), + InvoiceAmount(amount), + if (features != Features.empty) InvoiceFeatures(features) else null, + InvoiceNodeId(nodeKey.publicKey()), + ) + additionalTlvs + val signature = signSchnorr( + signatureTag, + rootHash(TlvStream(tlvs, request.records.unknown + customTlvs)), + nodeKey + ) + return Bolt12Invoice(TlvStream(tlvs + Signature(signature), request.records.unknown + customTlvs)) + } + + fun validate(records: TlvStream): Either { + when (val invoiceRequest = InvoiceRequest.validate(filterInvoiceRequestFields(records))) { + is Either.Left -> return Either.Left(invoiceRequest.value) + is Either.Right -> {} + } + if (records.get() == null) return Either.Left(MissingRequiredTlv(170)) + if (records.get()?.paths?.isEmpty() != false) return Either.Left(MissingRequiredTlv(160)) + if (records.get()?.paymentInfos?.size != records.get()?.paths?.size) return Either.Left(MissingRequiredTlv(162)) + if (records.get() == null) return Either.Left(MissingRequiredTlv(176)) + if (records.get() == null) return Either.Left(MissingRequiredTlv(164)) + if (records.get() == null) return Either.Left(MissingRequiredTlv(168)) + if (records.get() == null) return Either.Left(MissingRequiredTlv(240)) + return Either.Right(Bolt12Invoice(records)) + } + + fun fromString(input: String): Try = runTrying { + val (prefix, encoded, encoding) = Bech32.decodeBytes(input.lowercase(), true) + require(prefix == hrp) + require(encoding == Bech32.Encoding.Beck32WithoutChecksum) + val tlvs = OfferTypes.Invoice.tlvSerializer.read(encoded) + when (val invoice = validate(tlvs)) { + is Either.Left -> throw IllegalArgumentException(invoice.value.toString()) + is Either.Right -> invoice.value + } + } + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt index a8c5a3c8e..761001425 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt @@ -80,19 +80,19 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPayment paymentPreimage: ByteVector32, amount: MilliSatoshi?, description: Either, - extraHops: List>, + extraHops: List>, expirySeconds: Long? = null, timestampSeconds: Long = currentTimestampSeconds() - ): PaymentRequest { + ): Bolt11Invoice { val paymentHash = Crypto.sha256(paymentPreimage).toByteVector32() logger.debug(mapOf("paymentHash" to paymentHash)) { "using routing hints $extraHops" } - val pr = PaymentRequest.create( + val pr = Bolt11Invoice.create( nodeParams.chainHash, amount, paymentHash, nodeParams.nodePrivateKey, description, - PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, nodeParams.features.invoiceFeatures(), randomBytes32(), // We always include a payment metadata in our invoices, which lets us test whether senders support it @@ -470,8 +470,8 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPayment return PayToOpenResponseCommand(PayToOpenResponse(payToOpenRequest.chainHash, payToOpenRequest.paymentHash, PayToOpenResponse.Result.Failure(encryptedReason))) } - private fun minFinalCltvExpiry(paymentRequest: PaymentRequest, currentBlockHeight: Int): CltvExpiry { - val minFinalCltvExpiryDelta = paymentRequest.minFinalExpiryDelta ?: PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA + private fun minFinalCltvExpiry(paymentRequest: Bolt11Invoice, currentBlockHeight: Int): CltvExpiry { + val minFinalCltvExpiryDelta = paymentRequest.minFinalExpiryDelta ?: Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA return minFinalCltvExpiryDelta.toCltvExpiry(currentBlockHeight.toLong()) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandler.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandler.kt index 8245319d6..6f112061b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandler.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandler.kt @@ -56,7 +56,7 @@ class OutgoingPaymentHandler(val nodeParams: NodeParams, val walletParams: Walle logger.warning { "payment amount must be positive (${request.amount})" } return Failure(request, FinalFailure.InvalidPaymentAmount.toPaymentFailure()) } - if (!nodeParams.features.areSupported(Features(request.paymentRequest.features).invoiceFeatures())) { + if (!nodeParams.features.areSupported(request.paymentRequest.features.invoiceFeatures())) { logger.warning { "invoice contains mandatory features that we don't support" } return Failure(request, FinalFailure.FeaturesNotSupported.toPaymentFailure()) } @@ -348,7 +348,7 @@ class OutgoingPaymentHandler(val nodeParams: NodeParams, val walletParams: Walle val finalExpiry = nodeParams.paymentRecipientExpiryParams.computeFinalExpiry(currentBlockHeight, minFinalExpiryDelta) val finalPayload = PaymentOnion.FinalPayload.createSinglePartPayload(request.amount, finalExpiry, request.paymentRequest.paymentSecret, request.paymentRequest.paymentMetadata) - val invoiceFeatures = Features(request.paymentRequest.features) + val invoiceFeatures = request.paymentRequest.features val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (invoiceFeatures.hasFeature(Feature.TrampolinePayment) || invoiceFeatures.hasFeature(Feature.ExperimentalTrampolinePayment)) { OutgoingPaymentPacket.buildPacket(request.paymentHash, trampolineRoute, finalPayload, OnionRoutingPacket.TrampolinePacketLength) } else { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt index d6b7962bc..1705c847a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt @@ -74,7 +74,7 @@ object OutgoingPaymentPacket { * - firstExpiry is the cltv expiry for the first trampoline node in the route * - the trampoline onion to include in final payload of a normal onion */ - fun buildTrampolineToLegacyPacket(invoice: PaymentRequest, hops: List, finalPayload: PaymentOnion.FinalPayload): Triple { + fun buildTrampolineToLegacyPacket(invoice: Bolt11Invoice, hops: List, finalPayload: PaymentOnion.FinalPayload): Triple { // NB: the final payload will never reach the recipient, since the next-to-last trampoline hop will convert that to a legacy payment // We use the smallest final payload possible, otherwise we may overflow the trampoline onion size. val dummyFinalPayload = PaymentOnion.FinalPayload.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, finalPayload.paymentSecret, null) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/PaymentRequest.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/PaymentRequest.kt new file mode 100644 index 000000000..fa64e298d --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/PaymentRequest.kt @@ -0,0 +1,14 @@ +package fr.acinq.lightning.payment + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.lightning.Features +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.utils.currentTimestampSeconds + +sealed interface PaymentRequest { + val amount: MilliSatoshi? + val paymentHash: ByteVector32 + val features: Features + + fun isExpired(currentTimestampSeconds: Long = currentTimestampSeconds()): Boolean +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt index 2af5af306..0840fc492 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt @@ -11,8 +11,10 @@ import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.ShortChannelId +import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.payment.PaymentRequest import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.toByteVector sealed class OnionPaymentPayloadTlv : Tlv { /** Amount to forward to the next node. */ @@ -110,7 +112,7 @@ sealed class OnionPaymentPayloadTlv : Tlv { * Invoice routing hints. Only included for intermediate trampoline nodes when they should convert to a legacy payment * because the final recipient doesn't support trampoline. */ - data class InvoiceRoutingInfo(val extraHops: List>) : OnionPaymentPayloadTlv() { + data class InvoiceRoutingInfo(val extraHops: List>) : OnionPaymentPayloadTlv() { override val tag: Long get() = InvoiceRoutingInfo.tag override fun write(out: Output) { for (routeHint in extraHops) { @@ -128,11 +130,11 @@ sealed class OnionPaymentPayloadTlv : Tlv { companion object : TlvValueReader { const val tag: Long = 66099 override fun read(input: Input): InvoiceRoutingInfo { - val extraHops = mutableListOf>() + val extraHops = mutableListOf>() while (input.availableBytes > 0) { val hopCount = LightningCodecs.byte(input) val extraHop = (0 until hopCount).map { - PaymentRequest.TaggedField.ExtraHop( + Bolt11Invoice.TaggedField.ExtraHop( PublicKey(LightningCodecs.bytes(input, 33)), ShortChannelId(LightningCodecs.u64(input)), MilliSatoshi(LightningCodecs.u32(input).toLong()), @@ -295,10 +297,10 @@ object PaymentOnion { NodeRelayPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amount), OnionPaymentPayloadTlv.OutgoingCltv(expiry), OnionPaymentPayloadTlv.OutgoingNodeId(nextNodeId))) /** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */ - fun createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: PaymentRequest): NodeRelayPayload { + fun createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice): NodeRelayPayload { // NB: we limit the number of routing hints to ensure we don't overflow the onion. // A better solution is to provide the routing hints outside the onion (in the `update_add_htlc` tlv stream). - val prunedRoutingHints = invoice.routingInfo.shuffled().fold(listOf()) { previous, current -> + val prunedRoutingHints = invoice.routingInfo.shuffled().fold(listOf()) { previous, current -> if (previous.flatMap { it.hints }.size + current.hints.size <= 4) { previous + current } else { @@ -313,7 +315,7 @@ object PaymentOnion { add(OnionPaymentPayloadTlv.OutgoingNodeId(targetNodeId)) add(OnionPaymentPayloadTlv.PaymentData(invoice.paymentSecret, totalAmount)) invoice.paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) } - add(OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features)) + add(OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features.toByteArray().toByteVector())) add(OnionPaymentPayloadTlv.InvoiceRoutingInfo(prunedRoutingHints)) } ) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt index 043a281c0..263800ceb 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt @@ -6,8 +6,7 @@ import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output -import fr.acinq.lightning.EncodedNodeId - +import fr.acinq.lightning.* sealed class RouteBlindingEncryptedDataTlv : Tlv { /** Some padding can be added to ensure all payloads are the same size to improve privacy. */ @@ -22,6 +21,18 @@ sealed class RouteBlindingEncryptedDataTlv : Tlv { } } + /** Id of the outgoing channel, used to identify the next node. */ + data class OutgoingChannelId(val shortChannelId: ShortChannelId) : RouteBlindingEncryptedDataTlv() { + override val tag: Long get() = OutgoingChannelId.tag + override fun write(out: Output) = LightningCodecs.writeInt64(shortChannelId.toLong(), out) + + companion object : TlvValueReader { + const val tag: Long = 2 + override fun read(input: Input): OutgoingChannelId = + OutgoingChannelId(ShortChannelId(LightningCodecs.int64(input))) + } + } + /** Id of the next node. */ data class OutgoingNodeId(val nodeId: EncodedNodeId) : RouteBlindingEncryptedDataTlv() { override val tag: Long get() = OutgoingNodeId.tag @@ -62,6 +73,59 @@ sealed class RouteBlindingEncryptedDataTlv : Tlv { NextBlinding(PublicKey(LightningCodecs.bytes(input, 33))) } } + + /** Information for the relaying node to build the next HTLC. */ + data class PaymentRelay(val cltvExpiryDelta: CltvExpiryDelta, val feeProportionalMillionths: Int, val feeBase: MilliSatoshi) : RouteBlindingEncryptedDataTlv() { + override val tag: Long get() = PaymentRelay.tag + override fun write(out: Output) { + LightningCodecs.writeU16(cltvExpiryDelta.toInt(), out) + LightningCodecs.writeU32(feeProportionalMillionths, out) + LightningCodecs.writeTU32(feeBase.msat.toInt(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 10 + override fun read(input: Input): PaymentRelay { + val cltvExpiryDelta = CltvExpiryDelta(LightningCodecs.u16(input)) + val feeProportionalMillionths = LightningCodecs.u32(input) + val feeBase = MilliSatoshi(LightningCodecs.tu32(input).toLong()) + return PaymentRelay(cltvExpiryDelta, feeProportionalMillionths, feeBase) + } + } + } + + /** Constraints for the relaying node to enforce to prevent probing. */ + data class PaymentConstraints(val maxCltvExpiry: CltvExpiry, val minAmount: MilliSatoshi) : RouteBlindingEncryptedDataTlv() { + override val tag: Long get() = PaymentConstraints.tag + override fun write(out: Output) { + LightningCodecs.writeU32(maxCltvExpiry.toLong().toInt(), out) + LightningCodecs.writeTU64(minAmount.msat, out) + } + + companion object : TlvValueReader { + const val tag: Long = 12 + override fun read(input: Input): PaymentConstraints { + val maxCltvExpiry = CltvExpiry(LightningCodecs.u32(input).toLong()) + val minAmount = MilliSatoshi(LightningCodecs.tu64(input)) + return PaymentConstraints(maxCltvExpiry, minAmount) + } + } + } + + /** + * Blinded routes constrain the features that can be used by relaying nodes to prevent probing. + * Without this mechanism nodes supporting features that aren't widely supported could easily be identified. + */ + data class AllowedFeatures(val features: Features) : RouteBlindingEncryptedDataTlv() { + override val tag: Long get() = AllowedFeatures.tag + override fun write(out: Output) = LightningCodecs.writeBytes(features.toByteArray(), out) + + companion object : TlvValueReader { + const val tag: Long = 14 + override fun read(input: Input): AllowedFeatures = + AllowedFeatures(Features(LightningCodecs.bytes(input, input.availableBytes))) + } + } } data class RouteBlindingEncryptedData(val records: TlvStream) { @@ -80,10 +144,14 @@ data class RouteBlindingEncryptedData(val records: TlvStream, - RouteBlindingEncryptedDataTlv.OutgoingNodeId.tag to RouteBlindingEncryptedDataTlv.OutgoingNodeId.Companion as TlvValueReader, - RouteBlindingEncryptedDataTlv.PathId.tag to RouteBlindingEncryptedDataTlv.PathId.Companion as TlvValueReader, - RouteBlindingEncryptedDataTlv.NextBlinding.tag to RouteBlindingEncryptedDataTlv.NextBlinding.Companion as TlvValueReader, + RouteBlindingEncryptedDataTlv.Padding.tag to RouteBlindingEncryptedDataTlv.Padding as TlvValueReader, + RouteBlindingEncryptedDataTlv.OutgoingChannelId.tag to RouteBlindingEncryptedDataTlv.OutgoingChannelId as TlvValueReader, + RouteBlindingEncryptedDataTlv.OutgoingNodeId.tag to RouteBlindingEncryptedDataTlv.OutgoingNodeId as TlvValueReader, + RouteBlindingEncryptedDataTlv.PathId.tag to RouteBlindingEncryptedDataTlv.PathId as TlvValueReader, + RouteBlindingEncryptedDataTlv.NextBlinding.tag to RouteBlindingEncryptedDataTlv.NextBlinding as TlvValueReader, + RouteBlindingEncryptedDataTlv.PaymentRelay.tag to RouteBlindingEncryptedDataTlv.PaymentRelay as TlvValueReader, + RouteBlindingEncryptedDataTlv.PaymentConstraints.tag to RouteBlindingEncryptedDataTlv.PaymentConstraints as TlvValueReader, + RouteBlindingEncryptedDataTlv.AllowedFeatures.tag to RouteBlindingEncryptedDataTlv.AllowedFeatures as TlvValueReader, ) ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/db/PaymentsDbTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/db/PaymentsDbTestsCommon.kt index 85e781ab0..86c3c3011 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/db/PaymentsDbTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/db/PaymentsDbTestsCommon.kt @@ -6,6 +6,7 @@ import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.channel.TooManyAcceptedHtlcs +import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.payment.FinalFailure import fr.acinq.lightning.payment.PaymentRequest import fr.acinq.lightning.tests.utils.LightningTestSuite @@ -385,20 +386,20 @@ class PaymentsDbTestsCommon : LightningTestSuite() { Feature.BasicMultiPartPayment to FeatureSupport.Optional ) - private fun createFixture(): Triple { + private fun createFixture(): Triple { val db = InMemoryPaymentsDb() val preimage = randomBytes32() val pr = createInvoice(preimage) return Triple(db, preimage, pr) } - private fun createInvoice(preimage: ByteVector32): PaymentRequest { - return PaymentRequest.create(Block.LivenetGenesisBlock.hash, 150_000.msat, Crypto.sha256(preimage).toByteVector32(), randomKey(), Either.Left("invoice"), CltvExpiryDelta(16), defaultFeatures) + private fun createInvoice(preimage: ByteVector32): Bolt11Invoice { + return Bolt11Invoice.create(Block.LivenetGenesisBlock.hash, 150_000.msat, Crypto.sha256(preimage).toByteVector32(), randomKey(), Either.Left("invoice"), CltvExpiryDelta(16), defaultFeatures) } - private fun createExpiredInvoice(preimage: ByteVector32 = randomBytes32()): PaymentRequest { + private fun createExpiredInvoice(preimage: ByteVector32 = randomBytes32()): Bolt11Invoice { val now = currentTimestampSeconds() - return PaymentRequest.create( + return Bolt11Invoice.create( Block.LivenetGenesisBlock.hash, 150_000.msat, Crypto.sha256(preimage).toByteVector32(), diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentRequestTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt11InvoiceTestsCommon.kt similarity index 82% rename from src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentRequestTestsCommon.kt rename to src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt11InvoiceTestsCommon.kt index 4b5e16404..f2f697813 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentRequestTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt11InvoiceTestsCommon.kt @@ -11,7 +11,7 @@ import fr.acinq.lightning.utils.* import fr.acinq.secp256k1.Hex import kotlin.test.* -class PaymentRequestTestsCommon : LightningTestSuite() { +class Bolt11InvoiceTestsCommon : LightningTestSuite() { private val priv = PrivateKey(Hex.decode("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734")) private val pub = priv.publicKey() val nodeId = pub @@ -22,42 +22,42 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `check minimal unit is used`() { - assertEquals('p', PaymentRequest.unit(1.msat)) - assertEquals('p', PaymentRequest.unit(99.msat)) - assertEquals('n', PaymentRequest.unit(100.msat)) - assertEquals('p', PaymentRequest.unit(101.msat)) - assertEquals('n', PaymentRequest.unit(1.sat.toMilliSatoshi())) - assertEquals('u', PaymentRequest.unit(100.sat.toMilliSatoshi())) - assertEquals('n', PaymentRequest.unit(101.sat.toMilliSatoshi())) - assertEquals('u', PaymentRequest.unit(1155400.sat.toMilliSatoshi())) - assertEquals('m', PaymentRequest.unit(1.mbtc.toMilliSatoshi())) - assertEquals('m', PaymentRequest.unit(10.mbtc.toMilliSatoshi())) - assertNull(PaymentRequest.unit(1.btc.toMilliSatoshi())) - assertEquals('m', PaymentRequest.unit(1100.mbtc.toMilliSatoshi())) - assertNull(PaymentRequest.unit(2.btc.toMilliSatoshi())) - assertNull(PaymentRequest.unit(10.btc.toMilliSatoshi())) + assertEquals('p', Bolt11Invoice.unit(1.msat)) + assertEquals('p', Bolt11Invoice.unit(99.msat)) + assertEquals('n', Bolt11Invoice.unit(100.msat)) + assertEquals('p', Bolt11Invoice.unit(101.msat)) + assertEquals('n', Bolt11Invoice.unit(1.sat.toMilliSatoshi())) + assertEquals('u', Bolt11Invoice.unit(100.sat.toMilliSatoshi())) + assertEquals('n', Bolt11Invoice.unit(101.sat.toMilliSatoshi())) + assertEquals('u', Bolt11Invoice.unit(1155400.sat.toMilliSatoshi())) + assertEquals('m', Bolt11Invoice.unit(1.mbtc.toMilliSatoshi())) + assertEquals('m', Bolt11Invoice.unit(10.mbtc.toMilliSatoshi())) + assertNull(Bolt11Invoice.unit(1.btc.toMilliSatoshi())) + assertEquals('m', Bolt11Invoice.unit(1100.mbtc.toMilliSatoshi())) + assertNull(Bolt11Invoice.unit(2.btc.toMilliSatoshi())) + assertNull(Bolt11Invoice.unit(10.btc.toMilliSatoshi())) } @Test fun `decode empty amount`() { - assertNull(PaymentRequest.decodeAmount("")) - assertNull(PaymentRequest.decodeAmount("0")) - assertNull(PaymentRequest.decodeAmount("0p")) - assertNull(PaymentRequest.decodeAmount("0n")) - assertNull(PaymentRequest.decodeAmount("0u")) - assertNull(PaymentRequest.decodeAmount("0m")) + assertNull(Bolt11Invoice.decodeAmount("")) + assertNull(Bolt11Invoice.decodeAmount("0")) + assertNull(Bolt11Invoice.decodeAmount("0p")) + assertNull(Bolt11Invoice.decodeAmount("0n")) + assertNull(Bolt11Invoice.decodeAmount("0u")) + assertNull(Bolt11Invoice.decodeAmount("0m")) } @Test fun `check that we can still decode non-minimal amount encoding`() { - assertEquals(PaymentRequest.decodeAmount("1000u"), 100000000.msat) - assertEquals(PaymentRequest.decodeAmount("1000000n"), 100000000.msat) - assertEquals(PaymentRequest.decodeAmount("1000000000p"), 100000000.msat) + assertEquals(Bolt11Invoice.decodeAmount("1000u"), 100000000.msat) + assertEquals(Bolt11Invoice.decodeAmount("1000000n"), 100000000.msat) + assertEquals(Bolt11Invoice.decodeAmount("1000000000p"), 100000000.msat) } @Test fun `reject sub-millisatoshi amounts`() { - assertFails { PaymentRequest.decodeAmount("1501p") } + assertFails { Bolt11Invoice.decodeAmount("1501p") } } @Test @@ -67,7 +67,7 @@ class PaymentRequestTestsCommon : LightningTestSuite() { 100_000_000_000_000.msat to "lnbcrt10001pj8wd3rsp5cv2vayxnm7d4783r0477rstzpkl7n4ftmalgu9v8akzf0nhqrs3qpp5vednenalh0v6gzxpzrdxf9cepv4274vc0tax5389cjq0zv9qvs9sdqqxqyjw5qcqp29qyysgqk5f8um72jlnw9unjltdgxw9e2fvec0cxq05tcwuen2jpu42q4p9pt2djk2ysu62nkpg49km59wrexm0wt3msevz53fr2tfnqxf5sdnqpu8th97", ) testCases.forEach { (amount, ref) -> - val invoice = PaymentRequest.read(ref).get() + val invoice = Bolt11Invoice.read(ref).get() assertEquals(amount, invoice.amount) val encoded = invoice.write() assertEquals(ref, encoded) @@ -77,11 +77,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me at 03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad`() { val ref = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, null) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals(pr.description, "Please consider supporting this project") @@ -94,11 +94,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `Please send 3 USD for a cup of coffee to the same peer within 1 minute`() { val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(250000000)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals(pr.description, "1 cup coffee") @@ -112,11 +112,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `Please send 250000 sat for a cup of nonsense to the same peer within one minute`() { val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(250000000)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals(pr.description, "ナンセンス 1杯") @@ -129,11 +129,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `Now send 24 USD for an entire list of things -- hashed`() { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(2000000000)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals( @@ -151,11 +151,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `The same on testnet with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP`() { val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lntb") assertEquals(pr.amount, MilliSatoshi(2000000000)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals( @@ -173,11 +173,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `On mainnet with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255`() { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(2000000000)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals( @@ -189,10 +189,10 @@ class PaymentRequestTestsCommon : LightningTestSuite() { assertEquals(pr.fallbackAddress, "1RustyRX2oai4EYYDpQGWvEL62BBGqN9T") assertEquals( pr.routingInfo, listOf( - PaymentRequest.TaggedField.RoutingInfo( + Bolt11Invoice.TaggedField.RoutingInfo( listOf( - PaymentRequest.TaggedField.ExtraHop(PublicKey(ByteVector("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")), ShortChannelId("66051x263430x1800"), 1.msat, 20, CltvExpiryDelta(3)), - PaymentRequest.TaggedField.ExtraHop(PublicKey(ByteVector("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")), ShortChannelId("197637x395016x2314"), 2.msat, 30, CltvExpiryDelta(4)) + Bolt11Invoice.TaggedField.ExtraHop(PublicKey(ByteVector("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")), ShortChannelId("66051x263430x1800"), 1.msat, 20, CltvExpiryDelta(3)), + Bolt11Invoice.TaggedField.ExtraHop(PublicKey(ByteVector("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")), ShortChannelId("197637x395016x2314"), 2.msat, 30, CltvExpiryDelta(4)) ) ) ) @@ -205,11 +205,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `On mainnet with fallback p2sh address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX`() { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(2000000000)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals( @@ -227,11 +227,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `On mainnet with fallback p2wpkh address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4`() { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(2000000000)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals( @@ -249,11 +249,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `On mainnet with fallback p2wsh address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3`() { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(2000000000)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals( @@ -271,11 +271,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `On mainnet with fallback p2wsh address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12`() { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(2000000000)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.paymentSecret, ByteVector32("1111111111111111111111111111111111111111111111111111111111111111")) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) @@ -288,7 +288,7 @@ class PaymentRequestTestsCommon : LightningTestSuite() { ) assertEquals(pr.fallbackAddress, "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") assertEquals(pr.minFinalExpiryDelta, CltvExpiryDelta(12)) - assertEquals(pr.features, ByteVector("4100")) + assertEquals(pr.features.toByteArray().toByteVector(), ByteVector("4100")) val check = pr.sign(priv).write() assertEquals(ref, check) } @@ -305,7 +305,7 @@ class PaymentRequestTestsCommon : LightningTestSuite() { // "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqz599y53s3ujmcfjp5xrdap68qxymkqphwsexhmhr8wdz5usdzkzrse33chw6dlp3jhuhge9ley7j2ayx36kawe7kmgg8sv5ugdyusdcqzn8z9x" ) refs.forEach { ref -> - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(2500000000L)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) @@ -313,7 +313,7 @@ class PaymentRequestTestsCommon : LightningTestSuite() { assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals(pr.description, "coffee beans") - assertEquals(pr.features, ByteVector("08000000000000000000004100")) + assertEquals(pr.features.toByteArray().toByteVector(), ByteVector("08000000000000000000004100")) val check = pr.sign(priv).write() assertEquals(ref.lowercase(), check) } @@ -322,7 +322,7 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `On mainnet please send 30 USD for coffee beans to the same peer which supports features 8 14 99 and 100 using secret 0x1111111111111111111111111111111111111111111111111111111111111111`() { val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(2500000000L)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) @@ -331,7 +331,7 @@ class PaymentRequestTestsCommon : LightningTestSuite() { assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals(pr.description, "coffee beans") assertNull(pr.fallbackAddress) - assertEquals(pr.features, ByteVector("18000000000000000000004100")) + assertEquals(pr.features.toByteArray().toByteVector(), ByteVector("18000000000000000000004100")) val check = pr.sign(priv).write() assertEquals(ref, check) } @@ -339,11 +339,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `On mainnet please send 96 7878 534 msat for a list of items within one week amount in pico BTC`() { val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(967878534)) assertEquals(pr.paymentHash, ByteVector32("462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1572468703L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals(pr.description, "Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items") @@ -353,9 +353,9 @@ class PaymentRequestTestsCommon : LightningTestSuite() { assertEquals( pr.routingInfo, listOf( - PaymentRequest.TaggedField.RoutingInfo( + Bolt11Invoice.TaggedField.RoutingInfo( listOf( - PaymentRequest.TaggedField.ExtraHop( + Bolt11Invoice.TaggedField.ExtraHop( PublicKey(ByteVector("03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7")), ShortChannelId("589390x3312x1"), 1000.msat, @@ -373,11 +373,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { @Test fun `On mainnet please send 1000000 sat with payment metadata 0x01fafaf0`() { val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc" - val pr = PaymentRequest.read(ref).get() + val pr = Bolt11Invoice.read(ref).get() assertEquals(pr.prefix, "lnbc") assertEquals(pr.amount, MilliSatoshi(1000000000)) assertEquals(pr.paymentHash, ByteVector32("0001020304050607080900010203040506070809000102030405060708090102")) - assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.PaymentMetadata to FeatureSupport.Mandatory).toByteArray().toByteVector()) + assertEquals(pr.features, Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.PaymentMetadata to FeatureSupport.Mandatory)) assertEquals(pr.timestampSeconds, 1496314658L) assertEquals(pr.nodeId, PublicKey.fromHex("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assertEquals(pr.paymentSecret, ByteVector32("1111111111111111111111111111111111111111111111111111111111111111")) @@ -411,30 +411,30 @@ class PaymentRequestTestsCommon : LightningTestSuite() { "lnbc1qqqqpqqnp4qqqlftcw9qqqqqqqqqqqqygh9qpp5qpp5s7zxqqqqcqpjpqqygh9qpp5s7zxqqqqcqpjpqqlqqqqqqqqqqqqcqqpqqqqqqqqqqqsqqqqqqqqdqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqlqqqcqpjptfqptfqptfqpqqqqqqqqqqqqqqqqqqq8ddm0a" ) refs.forEach { - assertTrue(PaymentRequest.read(it).isFailure) + assertTrue(Bolt11Invoice.read(it).isFailure) } } @Test fun `ignore unknown tags`() { - val pr = PaymentRequest( + val pr = Bolt11Invoice( prefix = "lntb", amount = 100_000.msat, timestampSeconds = currentTimestampSeconds(), nodeId = nodeId, tags = listOf( - PaymentRequest.TaggedField.PaymentHash(ByteVector32.One), - PaymentRequest.TaggedField.Description("description"), - PaymentRequest.TaggedField.PaymentSecret(randomBytes32()), - PaymentRequest.TaggedField.Features(ByteVector("4100")), - PaymentRequest.TaggedField.UnknownTag(21, Bech32.eight2five("some data we don't understand".encodeToByteArray()).toList()) + Bolt11Invoice.TaggedField.PaymentHash(ByteVector32.One), + Bolt11Invoice.TaggedField.Description("description"), + Bolt11Invoice.TaggedField.PaymentSecret(randomBytes32()), + Bolt11Invoice.TaggedField.Features(ByteVector("4100")), + Bolt11Invoice.TaggedField.UnknownTag(21, Bech32.eight2five("some data we don't understand".encodeToByteArray()).toList()) ), signature = ByteVector.empty ).sign(priv) val serialized = pr.write() - val pr1 = PaymentRequest.read(serialized).get() - val unknownTag = pr1.tags.find { it is PaymentRequest.TaggedField.UnknownTag } + val pr1 = Bolt11Invoice.read(serialized).get() + val unknownTag = pr1.tags.find { it is Bolt11Invoice.TaggedField.UnknownTag } assertEquals(21, unknownTag!!.tag) } @@ -444,8 +444,8 @@ class PaymentRequestTestsCommon : LightningTestSuite() { mapOf(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.ShutdownAnySegwit to FeatureSupport.Optional), setOf(UnknownFeature(103), UnknownFeature(256)) ) - val pr = PaymentRequest.create(Block.LivenetGenesisBlock.hash, 500.msat, randomBytes32(), randomKey(), Either.Left("non-invoice features"), CltvExpiryDelta(6), nodeFeatures) - assertEquals(Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory), Features(pr.features)) + val pr = Bolt11Invoice.create(Block.LivenetGenesisBlock.hash, 500.msat, randomBytes32(), randomKey(), Either.Left("non-invoice features"), CltvExpiryDelta(6), nodeFeatures) + assertEquals(Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory), pr.features) } @Test @@ -456,9 +456,9 @@ class PaymentRequestTestsCommon : LightningTestSuite() { mapOf(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory), setOf(UnknownFeature(121), UnknownFeature(156)) ) - val pr = PaymentRequest.read(createInvoiceUnsafe(features = features).write()).get() - assertEquals(Features(pr.features), features) - assertEquals(Features(pr.features).invoiceFeatures(), features.remove(Feature.AnchorOutputs)) + val pr = Bolt11Invoice.read(createInvoiceUnsafe(features = features).write()).get() + assertEquals(pr.features, features) + assertEquals(pr.features.invoiceFeatures(), features.remove(Feature.AnchorOutputs)) } @Test @@ -483,29 +483,29 @@ class PaymentRequestTestsCommon : LightningTestSuite() { ) testCases.forEach { - assertEquals(it.second, PaymentRequest.TaggedField.Features.decode(it.first).bits) + assertEquals(it.second, Bolt11Invoice.TaggedField.Features.decode(it.first).bits) } } @Test fun `payment secret`() { val features = Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.BasicMultiPartPayment to FeatureSupport.Optional) - val pr = PaymentRequest.create(Block.LivenetGenesisBlock.hash, 123.msat, ByteVector32.One, priv, Either.Left("Some invoice"), CltvExpiryDelta(18), features) + val pr = Bolt11Invoice.create(Block.LivenetGenesisBlock.hash, 123.msat, ByteVector32.One, priv, Either.Left("Some invoice"), CltvExpiryDelta(18), features) assertNotNull(pr.paymentSecret) - assertEquals(ByteVector("024100"), pr.features) + assertEquals(ByteVector("024100"), pr.features.toByteArray().toByteVector()) - val pr1 = PaymentRequest.read(pr.write()).get() + val pr1 = Bolt11Invoice.read(pr.write()).get() assertEquals(pr1.paymentSecret, pr.paymentSecret) // An invoice without the payment secret feature should be rejected - assertTrue(PaymentRequest.read("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl").isFailure) + assertTrue(Bolt11Invoice.read("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl").isFailure) // An invoice that sets the payment secret feature bit must provide a payment secret. - assertTrue(PaymentRequest.read("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7").isFailure) + assertTrue(Bolt11Invoice.read("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7").isFailure) // Invoices must use a payment secret. assertFails { - PaymentRequest.create( + Bolt11Invoice.create( Block.LivenetGenesisBlock.hash, 123.msat, ByteVector32.One, @@ -521,11 +521,11 @@ class PaymentRequestTestsCommon : LightningTestSuite() { fun `invoice with descriptionHash`() { val descriptionHash = randomBytes32() val features = Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.BasicMultiPartPayment to FeatureSupport.Optional) - val pr = PaymentRequest.create(Block.LivenetGenesisBlock.hash, 123.msat, ByteVector32.One, priv, Either.Right(descriptionHash), CltvExpiryDelta(18), features) + val pr = Bolt11Invoice.create(Block.LivenetGenesisBlock.hash, 123.msat, ByteVector32.One, priv, Either.Right(descriptionHash), CltvExpiryDelta(18), features) assertNotNull(pr.descriptionHash) assertNull(pr.description) - val pr1 = PaymentRequest.read(pr.write()).get() + val pr1 = Bolt11Invoice.read(pr.write()).get() assertEquals(pr1.descriptionHash, pr.descriptionHash) assertNull(pr1.description) } @@ -541,22 +541,22 @@ class PaymentRequestTestsCommon : LightningTestSuite() { paymentSecret: ByteVector32 = randomBytes32(), paymentMetadata: ByteVector? = null, expirySeconds: Long? = null, - extraHops: List> = listOf(), + extraHops: List> = listOf(), timestampSeconds: Long = currentTimestampSeconds() - ): PaymentRequest { + ): Bolt11Invoice { val tags = mutableListOf( - PaymentRequest.TaggedField.PaymentHash(paymentHash), - PaymentRequest.TaggedField.Description(description), - PaymentRequest.TaggedField.MinFinalCltvExpiry(minFinalCltvExpiryDelta.toLong()), - PaymentRequest.TaggedField.PaymentSecret(paymentSecret), - PaymentRequest.TaggedField.Features(features.toByteArray().toByteVector()) + Bolt11Invoice.TaggedField.PaymentHash(paymentHash), + Bolt11Invoice.TaggedField.Description(description), + Bolt11Invoice.TaggedField.MinFinalCltvExpiry(minFinalCltvExpiryDelta.toLong()), + Bolt11Invoice.TaggedField.PaymentSecret(paymentSecret), + Bolt11Invoice.TaggedField.Features(features.toByteArray().toByteVector()) ) - paymentMetadata?.let { tags.add(PaymentRequest.TaggedField.PaymentMetadata(it)) } - expirySeconds?.let { tags.add(PaymentRequest.TaggedField.Expiry(it)) } + paymentMetadata?.let { tags.add(Bolt11Invoice.TaggedField.PaymentMetadata(it)) } + expirySeconds?.let { tags.add(Bolt11Invoice.TaggedField.Expiry(it)) } if (extraHops.isNotEmpty()) { - extraHops.forEach { tags.add(PaymentRequest.TaggedField.RoutingInfo(it)) } + extraHops.forEach { tags.add(Bolt11Invoice.TaggedField.RoutingInfo(it)) } } - return PaymentRequest( + return Bolt11Invoice( prefix = "lnbcrt", amount = amount, timestampSeconds = timestampSeconds, diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt new file mode 100644 index 000000000..2357d8e35 --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt @@ -0,0 +1,543 @@ +package fr.acinq.lightning.payment + +import fr.acinq.bitcoin.* +import fr.acinq.lightning.* +import fr.acinq.lightning.Lightning.randomBytes32 +import fr.acinq.lightning.Lightning.randomBytes64 +import fr.acinq.lightning.Lightning.randomKey +import fr.acinq.lightning.crypto.RouteBlinding +import fr.acinq.lightning.payment.Bolt12Invoice.Companion.PaymentBlindedContactInfo +import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.utils.currentTimestampSeconds +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.toByteVector +import fr.acinq.lightning.wire.* +import fr.acinq.lightning.wire.OfferTypes.ContactInfo +import fr.acinq.lightning.wire.OfferTypes.FallbackAddress +import fr.acinq.lightning.wire.OfferTypes.InvoiceAmount +import fr.acinq.lightning.wire.OfferTypes.InvoiceBlindedPay +import fr.acinq.lightning.wire.OfferTypes.InvoiceCreatedAt +import fr.acinq.lightning.wire.OfferTypes.InvoiceFallbacks +import fr.acinq.lightning.wire.OfferTypes.InvoiceFeatures +import fr.acinq.lightning.wire.OfferTypes.InvoiceNodeId +import fr.acinq.lightning.wire.OfferTypes.InvoicePaths +import fr.acinq.lightning.wire.OfferTypes.InvoicePaymentHash +import fr.acinq.lightning.wire.OfferTypes.InvoiceRelativeExpiry +import fr.acinq.lightning.wire.OfferTypes.InvoiceRequest +import fr.acinq.lightning.wire.OfferTypes.InvoiceRequestAmount +import fr.acinq.lightning.wire.OfferTypes.InvoiceRequestChain +import fr.acinq.lightning.wire.OfferTypes.InvoiceRequestMetadata +import fr.acinq.lightning.wire.OfferTypes.InvoiceRequestPayerId +import fr.acinq.lightning.wire.OfferTypes.InvoiceRequestPayerNote +import fr.acinq.lightning.wire.OfferTypes.InvoiceRequestQuantity +import fr.acinq.lightning.wire.OfferTypes.InvoiceTlv +import fr.acinq.lightning.wire.OfferTypes.Offer +import fr.acinq.lightning.wire.OfferTypes.OfferAmount +import fr.acinq.lightning.wire.OfferTypes.OfferChains +import fr.acinq.lightning.wire.OfferTypes.OfferDescription +import fr.acinq.lightning.wire.OfferTypes.OfferFeatures +import fr.acinq.lightning.wire.OfferTypes.OfferIssuer +import fr.acinq.lightning.wire.OfferTypes.OfferNodeId +import fr.acinq.lightning.wire.OfferTypes.OfferQuantityMax +import fr.acinq.lightning.wire.OfferTypes.PaymentInfo +import fr.acinq.lightning.wire.OfferTypes.Signature +import fr.acinq.lightning.wire.OfferTypes.rootHash +import fr.acinq.lightning.wire.OfferTypes.signSchnorr +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class Bolt12InvoiceTestsCommon : LightningTestSuite() { + + private fun signInvoiceTlvs(tlvs: TlvStream, key: PrivateKey): TlvStream { + val signature = signSchnorr(Bolt12Invoice.signatureTag, rootHash(tlvs), key) + return tlvs.copy(records = tlvs.records + Signature(signature)) + } + + private fun signInvoice(invoice: Bolt12Invoice, key: PrivateKey): Bolt12Invoice { + val tlvs = OfferTypes.removeSignature(invoice.records) + val signedInvoice = Bolt12Invoice(signInvoiceTlvs(tlvs, key)) + assertTrue(signedInvoice.checkSignature()) + return signedInvoice + } + + private fun createPaymentBlindedRoute( + nodeId: PublicKey, + sessionKey: PrivateKey = randomKey(), + pathId: ByteVector = randomBytes32() + ): PaymentBlindedContactInfo { + val selfPayload = RouteBlindingEncryptedData.tlvSerializer.write( + TlvStream( + RouteBlindingEncryptedDataTlv.PathId(pathId), + RouteBlindingEncryptedDataTlv.PaymentConstraints(CltvExpiry(1234567), 0.msat), + RouteBlindingEncryptedDataTlv.AllowedFeatures(Features.empty) + ) + ).toByteVector() + return PaymentBlindedContactInfo( + ContactInfo.BlindedPath( + RouteBlinding.create( + sessionKey, + listOf(nodeId), + listOf(selfPayload) + ) + ), PaymentInfo(1.msat, 2, CltvExpiryDelta(3), 4.msat, 5.msat, Features.empty) + ) + } + + @Test + fun `check invoice signature`() { + val nodeKey = randomKey() + val payerKey = randomKey() + val chain = BlockHash(randomBytes32()) + val offer = Offer(10000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) + val request = InvoiceRequest(offer, 11000.msat, 1, Features.empty, payerKey, chain) + val invoice = Bolt12Invoice( + request, + randomBytes32(), + nodeKey, + 300, + Features.empty, + listOf(createPaymentBlindedRoute(nodeKey.publicKey())) + ) + assertTrue(invoice.checkSignature()) + assertEquals(Bolt12Invoice.fromString(invoice.toString()).get().toString(), invoice.toString()) + // changing signature makes check fail + val withInvalidSignature = Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is Signature -> Signature(randomBytes64()) + else -> it + } + }.toSet(), invoice.records.unknown)) + assertFalse(withInvalidSignature.checkSignature()) + // changing fields makes the signature invalid + val withModifiedUnknownTlv = Bolt12Invoice(invoice.records.copy(unknown = setOf(GenericTlv(7, ByteVector.fromHex("ade4"))))) + assertFalse(withModifiedUnknownTlv.checkSignature()) + val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is OfferAmount -> OfferAmount(it.amount + 100.msat) + else -> it + } + }.toSet(), invoice.records.unknown)) + assertFalse(withModifiedAmount.checkSignature()) + } + + @Test + fun `check invoice signature with unknown field from invoice request`() { + val nodeKey = randomKey() + val payerKey = randomKey() + val chain = BlockHash(randomBytes32()) + val offer = Offer(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, setOf(GenericTlv(87, ByteVector.fromHex("0404"))))) + val invoice = Bolt12Invoice( + requestWithUnknownTlv, + randomBytes32(), + nodeKey, + 300, + Features.empty, + listOf(createPaymentBlindedRoute(nodeKey.publicKey())) + ) + assertEquals(invoice.records.unknown, setOf(GenericTlv(87, ByteVector.fromHex("0404")))) + println(invoice.validateFor(requestWithUnknownTlv)) + assertTrue(invoice.validateFor(requestWithUnknownTlv).isRight) + assertEquals(Bolt12Invoice.fromString(invoice.toString()).get().toString(), invoice.toString()) + } + + @Test + fun `check that invoice matches offer`() { + val nodeKey = randomKey() + val payerKey = randomKey() + val chain = BlockHash(randomBytes32()) + val offer = Offer(10000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) + val request = InvoiceRequest(offer, 11000.msat, 1, Features.empty, payerKey, chain) + val invoice = Bolt12Invoice( + request, + randomBytes32(), + nodeKey, + 300, + Features.empty, + listOf(createPaymentBlindedRoute(nodeKey.publicKey())) + ) + assertTrue(invoice.validateFor(request).isRight) + // amount must match the request + val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is OfferAmount -> OfferAmount(9000.msat) + else -> it + } + }.toSet())), nodeKey) + assertTrue(withOtherAmount.validateFor(request).isLeft) + // description must match the offer + val withOtherDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is OfferDescription -> OfferDescription("other description") + else -> it + } + }.toSet())), nodeKey) + assertTrue(withOtherDescription.validateFor(request).isLeft) + // nodeId must match the offer + val otherNodeKey = randomKey() + val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is OfferNodeId -> OfferNodeId(otherNodeKey.publicKey()) + else -> it + } + }.toSet())), nodeKey) + assertTrue(withOtherNodeId.validateFor(request).isLeft) + // issuer must match the offer + val withOtherIssuer = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records + OfferIssuer("spongebob"))), nodeKey) + assertTrue(withOtherIssuer.validateFor(request).isLeft) + } + + @Test + fun `check that invoice matches invoice request`() { + val nodeKey = randomKey() + val payerKey = randomKey() + val chain = BlockHash(randomBytes32()) + val offer = Offer(15000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) + val request = InvoiceRequest(offer, 15000.msat, 1, Features.empty, payerKey, chain) + assertTrue(request.quantity_opt == null) // when paying for a single item, the quantity field must not be present + val invoice = Bolt12Invoice( + request, + randomBytes32(), + nodeKey, + 300, + Features(Feature.BasicMultiPartPayment to FeatureSupport.Optional), + listOf(createPaymentBlindedRoute(nodeKey.publicKey())) + ) + assertTrue(invoice.validateFor(request).isRight) + val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is InvoiceFeatures -> InvoiceFeatures(Features(Feature.BasicMultiPartPayment to FeatureSupport.Mandatory)) + else -> it + } + }.toSet())), nodeKey) + assertTrue(withInvalidFeatures.validateFor(request).isLeft) + val withAmountTooBig = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is InvoiceRequestAmount -> InvoiceRequestAmount(20000.msat) + else -> it + } + }.toSet())), nodeKey) + assertTrue(withAmountTooBig.validateFor(request).isLeft) + val withQuantity = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records + InvoiceRequestQuantity(2))), nodeKey) + assertTrue(withQuantity.validateFor(request).isLeft) + val withOtherPayerKey = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is InvoiceRequestPayerId -> InvoiceRequestPayerId(randomKey().publicKey()) + else -> it + } + }.toSet())), nodeKey) + assertTrue(withOtherPayerKey.validateFor(request).isLeft) + val withPayerNote = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records + InvoiceRequestPayerNote("I am Batman"))), nodeKey) + assertTrue(withPayerNote.validateFor(request).isLeft) + val withOtherMetadata = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is InvoiceRequestMetadata -> InvoiceRequestMetadata(ByteVector.fromHex("ae46c46b86")) + else -> it + } + }.toSet())), nodeKey) + assertTrue(withOtherMetadata.validateFor(request).isLeft) + // Invoice request with more details about the payer. + val tlvs = setOf( + InvoiceRequestMetadata(ByteVector.fromHex("010203040506")), + OfferDescription("offer description"), + OfferNodeId(nodeKey.publicKey()), + InvoiceRequestAmount(15000.msat), + InvoiceRequestPayerId(payerKey.publicKey()), + InvoiceRequestPayerNote("I am Batman"), + OfferFeatures(Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory)) + ) + val signature = + signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs)), payerKey) + val requestWithPayerDetails = InvoiceRequest(TlvStream(tlvs + Signature(signature))) + val withPayerDetails = Bolt12Invoice( + requestWithPayerDetails, + randomBytes32(), + nodeKey, + 300, + Features.empty, + listOf(createPaymentBlindedRoute(nodeKey.publicKey())) + ) + assertTrue(withPayerDetails.validateFor(requestWithPayerDetails).isRight) + assertTrue(withPayerDetails.validateFor(request).isLeft) + val withOtherPayerNote = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { + when (it) { + is InvoiceRequestPayerNote -> InvoiceRequestPayerNote("Or am I Bruce Wayne?") + else -> it + } + }.toSet())), nodeKey) + assertTrue(withOtherPayerNote.validateFor(requestWithPayerDetails).isLeft) + assertTrue(withOtherPayerNote.validateFor(request).isLeft) + } + + @Test + fun `check invoice expiry`() { + val nodeKey = randomKey() + val payerKey = randomKey() + val chain = BlockHash(randomBytes32()) + val offer = Offer(5000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) + val request = InvoiceRequest(offer, 5000.msat, 1, Features.empty, payerKey, chain) + val invoice = Bolt12Invoice( + request, + randomBytes32(), + nodeKey, + 300, + Features.empty, + listOf(createPaymentBlindedRoute(nodeKey.publicKey())) + ) + assertFalse(invoice.isExpired()) + assertTrue(invoice.validateFor(request).isRight) + val expiredInvoice1 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is InvoiceCreatedAt -> InvoiceCreatedAt(0) + else -> it + } + }.toSet())), nodeKey) + assertTrue(expiredInvoice1.isExpired()) + assertTrue(expiredInvoice1.validateFor(request).isLeft) // when an invoice is expired, we mark it as invalid as well + val expiredInvoice2 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { + when (it) { + is InvoiceCreatedAt -> InvoiceCreatedAt(currentTimestampSeconds() - 2000) + is InvoiceRelativeExpiry -> InvoiceRelativeExpiry(1800) + else -> it + } + }.toSet())), nodeKey) + assertTrue(expiredInvoice2.isExpired()) + assertTrue(expiredInvoice2.validateFor(request).isLeft) // when an invoice is expired, we mark it as invalid as well + } + + @Test + fun `decode invalid invoice`() { + val nodeKey = randomKey() + val tlvs = setOf( + InvoiceRequestMetadata(ByteVector.fromHex("012345")), + OfferDescription("minimal invoice"), + OfferNodeId(nodeKey.publicKey()), + InvoiceRequestPayerId(randomKey().publicKey()), + InvoicePaths(listOf(createPaymentBlindedRoute(randomKey().publicKey()).route)), + InvoiceBlindedPay(listOf(PaymentInfo(0.msat, 0, CltvExpiryDelta(0), 0.msat, 765432.msat, Features.empty))), + InvoiceCreatedAt(123456789L), + InvoicePaymentHash(randomBytes32()), + InvoiceAmount(1684.msat), + InvoiceNodeId(nodeKey.publicKey()) + ) + // This minimal invoice is valid. + val signed = signInvoiceTlvs(TlvStream(tlvs), nodeKey) + val signedEncoded = Bech32.encodeBytes(Bolt12Invoice.hrp, OfferTypes.Invoice.tlvSerializer.write(signed), Bech32.Encoding.Beck32WithoutChecksum) + Bolt12Invoice.fromString(signedEncoded) + // But removing any TLV makes it invalid. + for (tlv in tlvs) { + val incomplete = tlvs.filterNot { it == tlv }.toSet() + val incompleteSigned = signInvoiceTlvs(TlvStream(incomplete), nodeKey) + val incompleteSignedEncoded = Bech32.encodeBytes(Bolt12Invoice.hrp, OfferTypes.Invoice.tlvSerializer.write(incompleteSigned), Bech32.Encoding.Beck32WithoutChecksum) + assertTrue(Bolt12Invoice.fromString(incompleteSignedEncoded).isFailure) + } + // Missing signature is also invalid. + val unsignedEncoded = Bech32.encodeBytes(Bolt12Invoice.hrp, OfferTypes.Invoice.tlvSerializer.write(TlvStream(tlvs)), Bech32.Encoding.Beck32WithoutChecksum) + assertTrue(Bolt12Invoice.fromString(unsignedEncoded).isFailure) + } + + @Test + fun `encode decode invoice with many fields`() { + val chain = Block.TestnetGenesisBlock.hash + val amount = 123456.msat + val description = "invoice with many fields" + val features = Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.RouteBlinding to FeatureSupport.Mandatory) + val issuer = "alice" + val nodeKey = PrivateKey.fromHex("998cf8ecab46f949bb960813b79d3317cabf4193452a211795cd8af1b9a25d90") + val path = createPaymentBlindedRoute( + nodeKey.publicKey(), + PrivateKey.fromHex("f0442c17bdd2cefe4a4ede210f163b068bb3fea6113ffacea4f322de7aa9737b"), + ByteVector.fromHex("76030536ba732cdc4e7bb0a883750bab2e88cb3dddd042b1952c44b4849c86bb") + ).copy(paymentInfo = PaymentInfo(2345.msat, 765, CltvExpiryDelta(324), 1000.msat, amount, Features.empty)) + val quantity = 57L + val payerKey = PublicKey.fromHex("024a8d96f4d13c4219f211b8a8e7b4ab7a898fd1b2e90274ca5a8737a9eda377f8") + val payerNote = "I'm Bob" + val payerInfo = ByteVector.fromHex("a9eb6e526eac59cd9b89fb20") + val createdAt = 1654654654L + val paymentHash = ByteVector32.fromValidHex("51951d4c53c904035f0b293dc9df1c0e7967213430ae07a5f3e134cd33325341") + val relativeExpiry = 3600L + val fallbacks = listOf(FallbackAddress(4, ByteVector.fromHex(("123d56f8"))), FallbackAddress(6, ByteVector.fromHex("eb3adc68945ef601"))) + val tlvs = TlvStream( + setOf( + InvoiceRequestMetadata(payerInfo), + OfferChains(listOf(chain)), + OfferAmount(amount), + OfferDescription(description), + OfferFeatures(Features.empty), + OfferIssuer(issuer), + OfferNodeId(nodeKey.publicKey()), + InvoiceRequestChain(chain), + InvoiceRequestAmount(amount), + InvoiceRequestQuantity(quantity), + InvoiceRequestPayerId(payerKey), + InvoiceRequestPayerNote(payerNote), + InvoicePaths(listOf(path.route)), + InvoiceBlindedPay(listOf(path.paymentInfo)), + InvoiceCreatedAt(createdAt), + InvoiceRelativeExpiry(relativeExpiry), + InvoicePaymentHash(paymentHash), + InvoiceAmount(amount), + InvoiceFallbacks(fallbacks), + InvoiceFeatures(Features.empty), + InvoiceNodeId(nodeKey.publicKey()), + ), setOf(GenericTlv(121, ByteVector.fromHex("010203")), GenericTlv(313, ByteVector.fromHex("baba"))) + ) + val signature = signSchnorr(Bolt12Invoice.signatureTag, rootHash(tlvs), nodeKey) + val invoice = Bolt12Invoice(tlvs.copy(records = tlvs.records + Signature(signature))) + assertTrue(invoice.toString() == "lni1qqx2n6mw2fh2ckwdnwylkgqzypp5jl7hlqnf2ugg7j3slkwwcwht57vhyzzwjr4dq84rxzgqqqqqqzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqqfq2ctvd93k293pq0zxw03kpc8tc2vv3kfdne0kntqhq8p70wtdncwq2zngaqp529mmc5pqgdyhl4lcy62hzz855v8annkr46a8n9eqsn5satgpagesjqqqqqq9yqcpufq9vqfetqssyj5djm6dz0zzr8eprw9gu762k75f3lgm96gzwn994peh48k6xalctyr5jfmdyppx7cneqvqsyqaq5qpugee7xc8qa0pf3jxe9k0976dvzuqu8eaedk0pcpg2dr5qx3gh00qzn8pc426xsh6l6ekdhr2hdpge0euhhp9frv6w04zjcqhhf6ru2wrqzqnjsxh8zmlm0gkeuq8qyxcy28uzhzljqkq22epc4mmdrx6vtm0eyyqr4agrvpkfuutftvf7f6paqewk3ysql3h8ukfz3phgmap5we4wsq3c97205a96r6f3hsd705jl29xt8yj3cu8vpm6z8lztjw3pcqqqpy5sqqqzl5q5gqqqqqqqqqqraqqqqqqqqqq7ysqqqzjqgc4qq6l2vqswzz5zq5v4r4x98jgyqd0sk2fae803crnevusngv9wq7jl8cf5e5eny56p4gpsrcjq4sfqgqqyzg74d7qxqqywkwkudz29aasp4cqtqggrc3nnudswp67znrydjtv7ta56c9cpc0nmjmv7rszs568gqdz3w770qsx3axhvq3e7npme2pwslgxa8kfcnqjqyeztg5r5wgzjpufjswx4crvd6kzlqjzukq5e707kp9ez98mj0zkckeggkm8cp6g6vgzh3j2q0lgp8ypt4ws") + val codedDecoded = Bolt12Invoice.fromString(invoice.toString()).get() + assertEquals(codedDecoded.invoiceRequest.chain, chain) + assertEquals(codedDecoded.amount, amount) + assertEquals(codedDecoded.description, description) + assertEquals(codedDecoded.features, features) + assertEquals(codedDecoded.invoiceRequest.offer.issuer, issuer) + assertEquals(codedDecoded.nodeId.value.drop(1), nodeKey.publicKey().value.drop(1)) + assertEquals(codedDecoded.blindedPaths, listOf(path)) + assertEquals(codedDecoded.invoiceRequest.quantity, quantity) + assertEquals(codedDecoded.invoiceRequest.payerId, payerKey) + assertEquals(codedDecoded.invoiceRequest.payerNote, payerNote) + assertEquals(codedDecoded.invoiceRequest.metadata, payerInfo) + assertEquals(codedDecoded.createdAtSeconds, createdAt) + assertEquals(codedDecoded.paymentHash, paymentHash) + assertEquals(codedDecoded.relativeExpirySeconds, relativeExpiry) + assertEquals(codedDecoded.fallbacks, fallbacks) + assertEquals(codedDecoded.records.unknown, setOf(GenericTlv(121, ByteVector.fromHex("010203")), GenericTlv(313, ByteVector.fromHex("baba")))) + } + + @Test + fun `minimal tip`() { + val nodeKey = PrivateKey.fromHex("48c6e5fcf499f50436f54c3b3edecdb0cb5961ca29d74bea5ab764828f08bf47") + assertEquals(nodeKey.publicKey(), PublicKey.fromHex("024ff5317f051c7f6eac0266c5cceaeb6c5775a940fab9854e47bfebf6bc7a0407")) + val payerKey = PrivateKey.fromHex("d817e8896c67d0bcabfdb93da7eb7fc698c829a181f994dd0ad866a8eda745e8") + assertEquals(payerKey.publicKey(), PublicKey.fromHex("031ef4439f638914de79220483dda32dfb7a431e799a5ce5a7643fbd70b2118e4e")) + val preimage = ByteVector32.fromValidHex("317d1fd8fec5f3ea23044983c2ba2a8043395b2a0790a815c9b12719aa5f1516") + val offer = Offer(null, "minimal tip", nodeKey.publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) + val encodedOffer = "lno1pg9k66twd9kkzmpqw35hq93pqf8l2vtlq5w87m4vqfnvtn82adk9wadfgratnp2wg7l7ha4u0gzqw" + assertEquals(offer.toString(), encodedOffer) + assertEquals(Offer.decode(encodedOffer).get(), offer) + val request = InvoiceRequest(offer, 12000000.msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + // Invoice request generation is not reproducible because we add randomness in the first TLV. + val encodedRequest = "lnr1qqs289chx8swkpmwf3uzexfxr0kk9syavsjcmkuur5qgjqt60ayjdec2pdkkjmnfd4skcgr5d9cpvggzfl6nzlc9r3lkatqzvmzue6htd3tht22ql2uc2nj8hl4ld0r6qsr4qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqpfq8dcmqpvzzqc773pe7cufzn08jgsys0w6xt0m0fp3u7v6tnj6weplh4ctyyvwfmcypemfjk6kryqxycnnmu2vp9tuw00eslf0grp6rf3hk6v76aynyn4lclra0fyyk2gxyf9hx73rnm775204tn8cltacw4s0fzd5c0lxm58s" + val decodedRequest = InvoiceRequest.decode(encodedRequest).get() + assertEquals(decodedRequest.unsigned().records.filterNot { it is InvoiceRequestMetadata }, request.unsigned().records.filterNot { it is InvoiceRequestMetadata }) + assertTrue(request.isValid()) + assertEquals(request.offer, offer) + val invoice = Bolt12Invoice( + decodedRequest, + preimage, + nodeKey, + 300, + Features.empty, + listOf(createPaymentBlindedRoute(nodeKey.publicKey())) + ) + assertEquals(Bolt12Invoice.fromString(invoice.toString()).get().records, invoice.records) + assertTrue(invoice.validateFor(decodedRequest).isRight) + // Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same. + val encodedInvoice = "lni1qqs289chx8swkpmwf3uzexfxr0kk9syavsjcmkuur5qgjqt60ayjdec2pdkkjmnfd4skcgr5d9cpvggzfl6nzlc9r3lkatqzvmzue6htd3tht22ql2uc2nj8hl4ld0r6qsr4qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqpfq8dcmqpvzzqc773pe7cufzn08jgsys0w6xt0m0fp3u7v6tnj6weplh4ctyyvwf6s2qqj075ch7pgu0ah2cqnxchxw46mv2a66js86hxz5u3ala0mtc7syqup2a4g7lywy0zytzjzdhlar5uegx8qj8el2a2hpl7z30cv56fxkhwqpqgpnv93lzfep3m5ppkt3jry0kanpk3uxku733nr03snlzqjls3pejqp65tnf8nf8te9h67ge0lgzum5kypuvqrdz50t238n6g0wrdtv49nrgjk7k26rw7a24arfx9z4dup8379etdpw0tfkg3mwtngsuqqqqqqgqqqqqyqqrqqqqqqqqqqqqgqqqqqqqqqqqq5qqpfqyvwv9m2dxqgqje2pqshlyweee7p4m365legtkdgvy6s02rdqsv38mwnmk8p88cz03dt725qahrvqtqggzfl6nzlc9r3lkatqzvmzue6htd3tht22ql2uc2nj8hl4ld0r6qsrlqsxuf5rcjutppkh79vr6q7vma5yccxhf79ghfg5zkc6z4u3zqzyh0nf50g7w7q4gk32hqg97pn7p9kaz0ddm5fza65ztdqj2sry3gw6l2" + val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get() + assertEquals(decodedInvoice.amount, invoice.amount) + assertEquals(decodedInvoice.nodeId, invoice.nodeId) + assertEquals(decodedInvoice.paymentHash, invoice.paymentHash) + assertEquals(decodedInvoice.description, invoice.description) + assertEquals(decodedInvoice.invoiceRequest.unsigned(), invoice.invoiceRequest.unsigned()) + } + + @Test + fun `minimal offer`() { + val nodeKey = PrivateKey.fromHex("3b7a19e8320bb86431cf92cd7c69cc1dc0181c37d5a09875e4603c4e37d3705d") + assertEquals(nodeKey.publicKey(), PublicKey.fromHex("03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5")) + val payerKey = PrivateKey.fromHex("0e00a9ef505292f90a0e8a7aa99d31750e885c42a3ef8866dd2bf97919aa3891") + assertEquals(payerKey.publicKey(), PublicKey.fromHex("033e94f2afd568d128f02ece844ad4a0a1ddf2a4e3a08beb2dba11b3f1134b0517")) + val preimage = ByteVector32.fromValidHex("09ad5e952ec39d45461ebdeceac206fb45574ae9054b5a454dd02c65f5ba1b7c") + val offer = Offer(456000000.msat, "minimal offer", nodeKey.publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) + val encodedOffer = "lno1pqzpktszqq9q6mtfde5k6ctvyphkven9wgtzzq7y3tyhuz0newawkdds924x6pet2aexssdrf5je2g2het9xpgw275" + assertEquals(offer.toString(), encodedOffer) + assertEquals(Offer.decode(encodedOffer).get(), offer) + val request = InvoiceRequest(offer, 456001234.msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + // Invoice request generation is not reproducible because we add randomness in the first TLV. + val encodedRequest = "lnr1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh7pq2hlvh7twke5830a44wc0zlrs2kph4ghndm60ahwcznhcd0pcpl332qv5xuemksazy3zx5s63kqmqkphrn9jg4ln55pc6syrwqukejeq" + val decodedRequest = InvoiceRequest.decode(encodedRequest).get() + assertEquals(decodedRequest.unsigned().records.filterNot { it is InvoiceRequestMetadata }, request.unsigned().records.filterNot { it is InvoiceRequestMetadata }) + assertTrue(request.isValid()) + assertEquals(request.offer, offer) + val invoice = Bolt12Invoice( + decodedRequest, + preimage, + nodeKey, + 300, + Features.empty, + listOf(createPaymentBlindedRoute(nodeKey.publicKey())) + ) + assertEquals(Bolt12Invoice.fromString(invoice.toString()).get().records, invoice.records) + assertTrue(invoice.validateFor(decodedRequest).isRight) + // Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same. + val encodedInvoice = "lni1qqsf4h8fsnpjkj057gjg9c3eqhv889440xh0z6f5kng9vsaad8pgq7sgqsdjuqsqpgxk66twd9kkzmpqdanxvetjzcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh42qsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqzjqsdjupkjtqssx05572ha26x39rczan5yft22pgwa72jw8gytavkm5ydn7yf5kpgh5zsq83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4qfjynufc627cuspz9lqzyk387xgzs4txcw0q97ugxfqm8x5zgj02gqgz4mnucmtxr620e5ttewtsg0s5n88euljnf7puagqje9j6gvaxk3pqqwsmahw79nhuq05zh8k29jk5qngpuny5l2vhjdrexg8hejukaee8fr7963dfag9q3lpcq9tt23f8s4h89cmjqa43u4fhk6l2y8qqqqqqzqqqqqpqqqcqqqqqqqqqqqzqqqqqqqqqqqq9qqq2gprrnp0zefszqyk2sgpvkrnmq53kv7r52rpnmtmd9ukredsnygsnymsurdy6e9la6l4hyz4qgxewqmftqggrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet6lqsy3s64amqgnlel7hn6fjrnk32xrn0ugr2xzct22ew28zftgmj70q9x2akqm34que8u2qe643cm38jpka6nfca4lfhuq6hgpnpwkpexrc" + val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get() + assertEquals(decodedInvoice.amount, invoice.amount) + assertEquals(decodedInvoice.nodeId, invoice.nodeId) + assertEquals(decodedInvoice.paymentHash, invoice.paymentHash) + assertEquals(decodedInvoice.description, invoice.description) + assertEquals(decodedInvoice.invoiceRequest.unsigned(), invoice.invoiceRequest.unsigned()) + } + + @Test + fun `offer with quantity`() { + val nodeKey = PrivateKey.fromHex("334a488858f260a2bb262493f6edcd35470f110bba62c7a5f90c78a047b364df") + assertEquals(nodeKey.publicKey(), PublicKey.fromHex("0327afd599da3226f4608b96ab042fe558bf558211d3c5e67ecc8be9963220434f")) + val payerKey = PrivateKey.fromHex("4b4129a801ea631e25903cd59dd7f7a6820c19d73aa0b095496e21027934becf") + assertEquals(payerKey.publicKey(), PublicKey.fromHex("027c6d03fa8f366e2ef8017cdfaf5d3cf1a3b0123db1318263b662c0aa9ec9c959")) + val preimage = ByteVector32.fromValidHex("99221825b86576e94391b179902be8b22c7cfa7c3d14aec6ae86657dfd9bd2a8") + val offer = Offer( + TlvStream( + OfferChains(listOf(Block.TestnetGenesisBlock.hash)), + OfferAmount(100000.msat), + OfferDescription("offer with quantity"), + OfferIssuer("alice@bigshop.com"), + OfferQuantityMax(1000), + OfferNodeId(nodeKey.publicKey()) + ) + ) + val encodedOffer = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2zdhkven9wgs8w6t5dqs8zatpde6xjarezggkzmrfvdj5qcnfvaeksmms9e3k7mg5qgp7s93pqvn6l4vemgezdarq3wt2kpp0u4vt74vzz8futen7ej97n93jypp57" + assertEquals(offer.toString(), encodedOffer) + assertEquals(Offer.decode(encodedOffer).get(), offer) + val request = InvoiceRequest(offer, 7200000.msat, 72, Features.empty, payerKey, Block.TestnetGenesisBlock.hash) + // Invoice request generation is not reproducible because we add randomness in the first TLV. + val encodedRequest = "lnr1qqs8lqvnh3kg9uj003lxlxyj8hthymgq4p9ms0ag0ryx5uw8gsuus4gzypp5jl7hlqnf2ugg7j3slkwwcwht57vhyzzwjr4dq84rxzgqqqqqqzqrqxr2qzsndanxvetjypmkjargypch2ctww35hg7gjz9skc6trv4qxy6t8wd5x7upwvdhk69qzq05pvggry7hatxw6xgn0gcytj64sgtl9tzl4tqs360z7vlkv305evv3qgd84qgzrf9la07pxj4cs3a9rplvuasawhfuewgyyay826q02xvysqqqqqpfqxmwaqptqzjzcyyp8cmgrl28nvm3wlqqheha0t570rgaszg7mzvvzvwmx9s92nmyujk0sgpef8dt57nygu3dnfhglymt6mnle6j8s28rler8wv3zygen07v4ddfplc9qs7nkdzwcelm2rs552slkpv45xxng65ne6y4dlq2764gqv" + val decodedRequest = InvoiceRequest.decode(encodedRequest).get() + assertEquals(decodedRequest.unsigned().records.filterNot { it is InvoiceRequestMetadata }, request.unsigned().records.filterNot { it is InvoiceRequestMetadata }) + assertTrue(request.isValid()) + assertEquals(request.offer, offer) + val invoice = Bolt12Invoice( + decodedRequest, + preimage, + nodeKey, + 300, + Features.empty, + listOf(createPaymentBlindedRoute(nodeKey.publicKey())) + ) + assertEquals(Bolt12Invoice.fromString(invoice.toString()).get().records, invoice.records) + assertTrue(invoice.validateFor(decodedRequest).isRight) + // Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same. + val encodedInvoice = "lni1qqs8lqvnh3kg9uj003lxlxyj8hthymgq4p9ms0ag0ryx5uw8gsuus4gzypp5jl7hlqnf2ugg7j3slkwwcwht57vhyzzwjr4dq84rxzgqqqqqqzqrqxr2qzsndanxvetjypmkjargypch2ctww35hg7gjz9skc6trv4qxy6t8wd5x7upwvdhk69qzq05pvggry7hatxw6xgn0gcytj64sgtl9tzl4tqs360z7vlkv305evv3qgd84qgzrf9la07pxj4cs3a9rplvuasawhfuewgyyay826q02xvysqqqqqpfqxmwaqptqzjzcyyp8cmgrl28nvm3wlqqheha0t570rgaszg7mzvvzvwmx9s92nmyujkdq5qpj0t74n8dryfh5vz9ed2cy9lj43064sgga830x0mxgh6vkxgsyxnczgew6pkkhja3cl3dfxthumcmp6gkp446ha4tcj884eqch6g57newqzquqmar5nynwtg9lknq98yzslwla3vdxefulhq2jkwnqnsf7umpl5cqr58qkj63hkpl7ffyd6f3qgn3m5kuegehhakvxw7fuw29tf3r5wgj37uecjdw2th4t5fp7f99xvk4f3gwl0wyf2a558wqa9w3pcqqqqqqsqqqqqgqqxqqqqqqqqqqqqsqqqqqqqqqqqpgqqzjqgcuctck2vqsp9j5zqlsxsv7uy23npygenelt4q5sdh8ftc3x7rpd0hqlachjnj9z834s4gpkmhgqkqssxfa06kva5v3x73sgh94tqsh72k9l2kppr579uelvezlfjcezqs607pqxa3afljxyf2ua9dlqs33wrfzakt5tpraklpzfpn63uxa7el475x4sc0w4hs75e3nhe689slfz4ldqlwja3zaq0w3mnz79f4ne0c3r3c" + val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get() + assertEquals(decodedInvoice.amount, invoice.amount) + assertEquals(decodedInvoice.nodeId, invoice.nodeId) + assertEquals(decodedInvoice.paymentHash, invoice.paymentHash) + assertEquals(decodedInvoice.description, invoice.description) + assertEquals(decodedInvoice.invoiceRequest.unsigned(), invoice.invoiceRequest.unsigned()) + } + + @Test + fun `cln invoice`() { + val encodedInvoice = "lni1qqgds4gweqxey37gexf5jus4kcrwuq3qqc3xu3s3rg94nj40zfsy866mhu5vxne6tcej5878k2mneuvgjy8s5predakx793pqfxv2rtqfajhp98c5tlsxxkkmzy0ntpzp2rtt9yum2495hqrq4wkj5pqqc3xu3s3rg94nj40zfsy866mhu5vxne6tcej5878k2mneuvgjy84yqucj6q9sggrnl24r93kfmdnatwpy72mxg7ygr9waxu0830kkpqx84pd5j65fhg2pxqzfnzs6cz0v4cff79zlup344kc3ru6cgs2s66ef8x64fd9cqc9t45s954fef6n3ql8urpc4r2vvunc0uv9yq37g485heph6lpuw34ywxadqypwq3hlcrpyk32zdvlrgfsdnx5jegumenll49v502862l9sq5erz3qqxte8tyk308ykd6fqy2lxkrsmeq77d8s5977pzmc68lgvs2xcn0kfvnlzud9fvkv900ggwe7yf9hf7lr6qz3pcqqqqqqqqqqqqqqq5qqqqqqqqqqqqqwjfvkl43fqqqqqqzjqgcuhrdv2sgq5spd8qp4ev2rw0v9r7cvvrntlzpvlwmd8vczycklu87336h55g24q8xykszczzqjvc5xkqnm9wz203ghlqvdddkyglxkzyz5xkk2fek42tfwqxp2ad8cypv26x5zxkyk675ep3v48grwydze6nvvg56cklgmvztuny58t5j0fl3hemx3lvd0ryx89jtf0h069z6r2qwqvjlyrewvzsfqmmfajs70q" + val invoice = Bolt12Invoice.fromString(encodedInvoice).get() + assertTrue(invoice.checkSignature()) + assertEquals(invoice.amount, 10000000.msat) + assertEquals(invoice.nodeId, PublicKey.fromHex("024cc50d604f657094f8a2ff031ad6d888f9ac220a86b5949cdaaa5a5c03055d69")) + assertEquals(invoice.paymentHash, ByteVector32.fromValidHex("14805a7006b96286e7b0a3f618c1cd7f1059f76da766044c5bfc3fa31d5e9442")) + assertTrue(invoice.description == "yolo") + } +} \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt index d1c4e25d5..63695a363 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt @@ -57,9 +57,9 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { ) // The following invoice requires payment_metadata. val invoice1 = - PaymentRequestTestsCommon.createInvoiceUnsafe(features = Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.PaymentMetadata to FeatureSupport.Mandatory)) + Bolt11InvoiceTestsCommon.createInvoiceUnsafe(features = Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.PaymentMetadata to FeatureSupport.Mandatory)) // The following invoice requires unknown feature bit 188. - val invoice2 = PaymentRequestTestsCommon.createInvoiceUnsafe(features = Features(mapOf(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory), setOf(UnknownFeature(188)))) + val invoice2 = Bolt11InvoiceTestsCommon.createInvoiceUnsafe(features = Features(mapOf(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory), setOf(UnknownFeature(188)))) for (invoice in listOf(invoice1, invoice2)) { val outgoingPaymentHandler = OutgoingPaymentHandler(alice.staticParams.nodeParams.copy(features = features), defaultWalletParams, InMemoryPaymentsDb()) val payment = SendPayment(UUID.randomUUID(), 15_000.msat, invoice.nodeId, invoice) @@ -246,13 +246,13 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { Feature.BasicMultiPartPayment to FeatureSupport.Optional, Feature.TrampolinePayment to FeatureSupport.Optional ) - PaymentRequest.create( + Bolt11Invoice.create( chainHash = Block.LivenetGenesisBlock.hash, amount = 195_000.msat, paymentHash = randomBytes32(), privateKey = recipientKey, description = Either.Left("trampoline backwards-compatibility"), - minFinalCltvExpiryDelta = PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, + minFinalCltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, features = Features(invoiceFeatures.toMap()), ) } @@ -260,7 +260,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { testSinglePartTrampolinePayment(payment, invoice, recipientKey) } - private suspend fun testSinglePartTrampolinePayment(payment: SendPayment, invoice: PaymentRequest, recipientKey: PrivateKey) { + private suspend fun testSinglePartTrampolinePayment(payment: SendPayment, invoice: Bolt11Invoice, recipientKey: PrivateKey) { val channels = makeChannels() val walletParams = defaultWalletParams.copy(trampolineFees = listOf(TrampolineFees(3.sat, 10_000, CltvExpiryDelta(144)))) val outgoingPaymentHandler = OutgoingPaymentHandler(TestConstants.Alice.nodeParams, walletParams, InMemoryPaymentsDb()) @@ -276,9 +276,9 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { val (outerB, innerB, packetC) = PaymentPacketTestsCommon.decryptNodeRelay(makeUpdateAddHtlc(channelId, add), TestConstants.Bob.nodeParams.nodePrivateKey) assertEquals(205_000.msat, outerB.amount) assertEquals(205_000.msat, outerB.totalAmount) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + CltvExpiryDelta(144) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, outerB.expiry) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + CltvExpiryDelta(144) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, outerB.expiry) assertEquals(200_000.msat, innerB.amountToForward) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, innerB.outgoingCltv) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, innerB.outgoingCltv) assertEquals(payment.recipient, innerB.outgoingNodeId) assertNull(innerB.invoiceRoutingInfo) assertNull(innerB.invoiceFeatures) @@ -288,7 +288,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { val payloadBytesC = Sphinx.peel(recipientKey, payment.paymentHash, packetC, OnionRoutingPacket.TrampolinePacketLength).right!! val payloadC = PaymentOnion.FinalPayload.read(payloadBytesC.payload.toByteArray()) assertEquals(200_000.msat, payloadC.amount) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, payloadC.expiry) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, payloadC.expiry) assertEquals(payloadC.amount, payloadC.totalAmount) assertEquals(invoice.paymentSecret, payloadC.paymentSecret) @@ -331,9 +331,9 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { val (outerB, innerB, packetC) = PaymentPacketTestsCommon.decryptNodeRelay(makeUpdateAddHtlc(channelId, add), TestConstants.Bob.nodeParams.nodePrivateKey) assertEquals(add.amount, outerB.amount) assertEquals(310_000.msat, outerB.totalAmount) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + CltvExpiryDelta(144) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, outerB.expiry) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + CltvExpiryDelta(144) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, outerB.expiry) assertEquals(300_000.msat, innerB.amountToForward) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, innerB.outgoingCltv) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, innerB.outgoingCltv) assertEquals(payment.recipient, innerB.outgoingNodeId) assertNull(innerB.invoiceRoutingInfo) assertNull(innerB.invoiceFeatures) @@ -343,7 +343,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { val payloadBytesC = Sphinx.peel(recipientKey, payment.paymentHash, packetC, OnionRoutingPacket.TrampolinePacketLength).right!! val payloadC = PaymentOnion.FinalPayload.read(payloadBytesC.payload.toByteArray()) assertEquals(300_000.msat, payloadC.amount) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, payloadC.expiry) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, payloadC.expiry) assertEquals(payloadC.amount, payloadC.totalAmount) assertEquals(invoice.paymentSecret, payloadC.paymentSecret) } @@ -377,7 +377,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { val walletParams = defaultWalletParams.copy(trampolineFees = listOf(TrampolineFees(10.sat, 0, CltvExpiryDelta(144)))) val outgoingPaymentHandler = OutgoingPaymentHandler(TestConstants.Alice.nodeParams, walletParams, InMemoryPaymentsDb()) val recipientKey = randomKey() - val extraHops = listOf(listOf(PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(42), 10.msat, 100, CltvExpiryDelta(48)))) + val extraHops = listOf(listOf(Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(42), 10.msat, 100, CltvExpiryDelta(48)))) val invoice = makeInvoice(amount = null, supportsTrampoline = false, privKey = recipientKey, extraHops = extraHops) val payment = SendPayment(UUID.randomUUID(), 300_000.msat, invoice.nodeId, invoice) @@ -392,12 +392,12 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { val (outerB, innerB, _) = PaymentPacketTestsCommon.decryptNodeRelay(makeUpdateAddHtlc(channelId, add), TestConstants.Bob.nodeParams.nodePrivateKey) assertEquals(add.amount, outerB.amount) assertEquals(310_000.msat, outerB.totalAmount) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + CltvExpiryDelta(144) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, outerB.expiry) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + CltvExpiryDelta(144) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, outerB.expiry) assertEquals(300_000.msat, innerB.amountToForward) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, innerB.outgoingCltv) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, innerB.outgoingCltv) assertEquals(payment.recipient, innerB.outgoingNodeId) assertEquals(invoice.paymentSecret, innerB.paymentSecret) - assertEquals(invoice.features, innerB.invoiceFeatures) + assertEquals(invoice.features.toByteArray().toByteVector(), innerB.invoiceFeatures) assertFalse(innerB.invoiceRoutingInfo.isNullOrEmpty()) assertEquals(invoice.routingInfo.map { it.hints }, innerB.invoiceRoutingInfo) } @@ -441,7 +441,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { // The trampoline node should receive the right forwarding information. val (outerB, innerB, packetC) = PaymentPacketTestsCommon.decryptNodeRelay(makeUpdateAddHtlc(channelId, add), TestConstants.Bob.nodeParams.nodePrivateKey) assertEquals(add.amount, outerB.amount) - val minFinalExpiry = CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA + recipientExpiryParams.min + val minFinalExpiry = CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA + recipientExpiryParams.min assertTrue(minFinalExpiry + CltvExpiryDelta(48) <= outerB.expiry) assertTrue(minFinalExpiry <= innerB.outgoingCltv) @@ -461,14 +461,14 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { val outgoingPaymentHandler = OutgoingPaymentHandler(TestConstants.Alice.nodeParams, walletParams, InMemoryPaymentsDb()) val recipientKey = randomKey() val extraHops = listOf( - listOf(PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(10), 10.msat, 100, CltvExpiryDelta(48))), - listOf(PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(12), 10.msat, 110, CltvExpiryDelta(48))), - listOf(PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(13), 10.msat, 120, CltvExpiryDelta(48))), - listOf(PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(14), 10.msat, 130, CltvExpiryDelta(48))), - listOf(PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(15), 10.msat, 140, CltvExpiryDelta(48))), - listOf(PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(16), 10.msat, 150, CltvExpiryDelta(48))), - listOf(PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(17), 10.msat, 160, CltvExpiryDelta(48))), - listOf(PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(18), 10.msat, 170, CltvExpiryDelta(48))), + listOf(Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(10), 10.msat, 100, CltvExpiryDelta(48))), + listOf(Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(12), 10.msat, 110, CltvExpiryDelta(48))), + listOf(Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(13), 10.msat, 120, CltvExpiryDelta(48))), + listOf(Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(14), 10.msat, 130, CltvExpiryDelta(48))), + listOf(Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(15), 10.msat, 140, CltvExpiryDelta(48))), + listOf(Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(16), 10.msat, 150, CltvExpiryDelta(48))), + listOf(Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(17), 10.msat, 160, CltvExpiryDelta(48))), + listOf(Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(18), 10.msat, 170, CltvExpiryDelta(48))), ) val invoice = makeInvoice(amount = 200_000.msat, supportsTrampoline = false, privKey = recipientKey, extraHops = extraHops) val payment = SendPayment(UUID.randomUUID(), 200_000.msat, invoice.nodeId, invoice) @@ -486,7 +486,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(200_000.msat, innerB.amountToForward) assertEquals(payment.recipient, innerB.outgoingNodeId) assertEquals(invoice.paymentSecret, innerB.paymentSecret) - assertEquals(invoice.features, innerB.invoiceFeatures) + assertEquals(invoice.features.toByteArray().toByteVector(), innerB.invoiceFeatures) // The trampoline node should receive a subset of the routing hints that fits inside the onion. assertEquals(4, innerB.invoiceRoutingInfo?.flatten()?.toSet()?.size) innerB.invoiceRoutingInfo?.flatten()?.forEach { assertTrue(extraHops.flatten().contains(it)) } @@ -512,7 +512,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { val payloadB = IncomingPaymentPacket.decrypt(makeUpdateAddHtlc(channelId, add), TestConstants.Bob.nodeParams.nodePrivateKey).right!! assertEquals(add.amount, payloadB.amount) assertEquals(300_000.msat, payloadB.totalAmount) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, payloadB.expiry) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, payloadB.expiry) assertEquals(invoice.paymentSecret, payloadB.paymentSecret) } @@ -574,9 +574,9 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { val (outerB, innerB, packetC) = PaymentPacketTestsCommon.decryptNodeRelay(makeUpdateAddHtlc(channelId, add), TestConstants.Bob.nodeParams.nodePrivateKey) assertEquals(add.amount, outerB.amount) assertEquals(301_030.msat, outerB.totalAmount) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + CltvExpiryDelta(576) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, outerB.expiry) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + CltvExpiryDelta(576) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, outerB.expiry) assertEquals(300_000.msat, innerB.amountToForward) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, innerB.outgoingCltv) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, innerB.outgoingCltv) assertEquals(payment.recipient, innerB.outgoingNodeId) assertNull(innerB.invoiceRoutingInfo) assertNull(innerB.invoiceFeatures) @@ -586,7 +586,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { val payloadBytesC = Sphinx.peel(recipientKey, payment.paymentHash, packetC, OnionRoutingPacket.TrampolinePacketLength).right!! val payloadC = PaymentOnion.FinalPayload.read(payloadBytesC.payload.toByteArray()) assertEquals(300_000.msat, payloadC.amount) - assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, payloadC.expiry) + assertEquals(CltvExpiry(TestConstants.defaultBlockHeight.toLong()) + Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, payloadC.expiry) assertEquals(payloadC.amount, payloadC.totalAmount) assertEquals(invoice.paymentSecret, payloadC.paymentSecret) } @@ -753,7 +753,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { } } - private suspend fun testLocalChannelFailures(invoice: PaymentRequest) { + private suspend fun testLocalChannelFailures(invoice: Bolt11Invoice) { val channels = makeChannels() val outgoingPaymentHandler = OutgoingPaymentHandler(TestConstants.Alice.nodeParams, defaultWalletParams, InMemoryPaymentsDb()) val payment = SendPayment(UUID.randomUUID(), 5_000.msat, invoice.nodeId, invoice) @@ -947,7 +947,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { } } - private fun makeInvoice(amount: MilliSatoshi?, supportsTrampoline: Boolean, privKey: PrivateKey = randomKey(), extraHops: List> = listOf()): PaymentRequest { + private fun makeInvoice(amount: MilliSatoshi?, supportsTrampoline: Boolean, privKey: PrivateKey = randomKey(), extraHops: List> = listOf()): Bolt11Invoice { val paymentPreimage: ByteVector32 = randomBytes32() val paymentHash = Crypto.sha256(paymentPreimage).toByteVector32() @@ -960,13 +960,13 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { invoiceFeatures[Feature.ExperimentalTrampolinePayment] = FeatureSupport.Optional } - return PaymentRequest.create( + return Bolt11Invoice.create( chainHash = Block.LivenetGenesisBlock.hash, amount = amount, paymentHash = paymentHash, privateKey = privKey, description = Either.Left("unit test"), - minFinalCltvExpiryDelta = PaymentRequest.DEFAULT_MIN_FINAL_EXPIRY_DELTA, + minFinalCltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, features = Features(invoiceFeatures.toMap()), extraHops = extraHops ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt index 7d50ca60a..114027192 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt @@ -277,16 +277,16 @@ class PaymentPacketTestsCommon : LightningTestSuite() { // / \ // a -> b -> c d -> e - val routingHints = listOf(PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(42), 10.msat, 100, CltvExpiryDelta(144))) + val routingHints = listOf(Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(42), 10.msat, 100, CltvExpiryDelta(144))) val invoiceFeatures = Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory, Feature.BasicMultiPartPayment to FeatureSupport.Optional) - val invoice = PaymentRequest( + val invoice = Bolt11Invoice( "lnbcrt", finalAmount, currentTimestampSeconds(), e, listOf( - PaymentRequest.TaggedField.PaymentHash(paymentHash), - PaymentRequest.TaggedField.PaymentSecret(paymentSecret), - PaymentRequest.TaggedField.PaymentMetadata(paymentMetadata), - PaymentRequest.TaggedField.DescriptionHash(randomBytes32()), - PaymentRequest.TaggedField.Features(invoiceFeatures.toByteArray().toByteVector()), - PaymentRequest.TaggedField.RoutingInfo(routingHints) + Bolt11Invoice.TaggedField.PaymentHash(paymentHash), + Bolt11Invoice.TaggedField.PaymentSecret(paymentSecret), + Bolt11Invoice.TaggedField.PaymentMetadata(paymentMetadata), + Bolt11Invoice.TaggedField.DescriptionHash(randomBytes32()), + Bolt11Invoice.TaggedField.Features(invoiceFeatures.toByteArray().toByteVector()), + Bolt11Invoice.TaggedField.RoutingInfo(routingHints) ), ByteVector.empty ) val (amountAC, expiryAC, trampolineOnion) = OutgoingPaymentPacket.buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.FinalPayload.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), null)) @@ -345,16 +345,16 @@ class PaymentPacketTestsCommon : LightningTestSuite() { @Test fun `fail to build a trampoline payment when too much invoice data is provided`() { - val extraHop = PaymentRequest.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(1), 10.msat, 100, CltvExpiryDelta(12)) + val extraHop = Bolt11Invoice.TaggedField.ExtraHop(randomKey().publicKey(), ShortChannelId(1), 10.msat, 100, CltvExpiryDelta(12)) val routingHintOverflow = listOf(extraHop, extraHop, extraHop, extraHop, extraHop, extraHop, extraHop) val featuresOverflow = ByteVector("010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101024100") - val invoice = PaymentRequest( + val invoice = Bolt11Invoice( "lnbcrt", finalAmount, currentTimestampSeconds(), e, listOf( - PaymentRequest.TaggedField.PaymentHash(paymentHash), - PaymentRequest.TaggedField.PaymentSecret(paymentSecret), - PaymentRequest.TaggedField.Features(featuresOverflow), - PaymentRequest.TaggedField.DescriptionHash(randomBytes32()), - PaymentRequest.TaggedField.RoutingInfo(routingHintOverflow) + Bolt11Invoice.TaggedField.PaymentHash(paymentHash), + Bolt11Invoice.TaggedField.PaymentSecret(paymentSecret), + Bolt11Invoice.TaggedField.Features(featuresOverflow), + Bolt11Invoice.TaggedField.DescriptionHash(randomBytes32()), + Bolt11Invoice.TaggedField.RoutingInfo(routingHintOverflow) ), ByteVector.empty ) assertFails { OutgoingPaymentPacket.buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.FinalPayload.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), null)) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt index 39bf5ddca..fa9acb53d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt @@ -8,6 +8,7 @@ import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.crypto.assertArrayEquals +import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.payment.PaymentRequest import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.msat @@ -82,8 +83,8 @@ class PaymentOnionTestsCommon : LightningTestSuite() { val node2 = PublicKey(Hex.decode("025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486")) val node3 = PublicKey(Hex.decode("02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee")) val routingHints = listOf( - listOf(PaymentRequest.TaggedField.ExtraHop(node1, ShortChannelId(1), 10.msat, 100, CltvExpiryDelta(144))), - listOf(PaymentRequest.TaggedField.ExtraHop(node2, ShortChannelId(2), 20.msat, 150, CltvExpiryDelta(12)), PaymentRequest.TaggedField.ExtraHop(node3, ShortChannelId(3), 30.msat, 200, CltvExpiryDelta(24))) + listOf(Bolt11Invoice.TaggedField.ExtraHop(node1, ShortChannelId(1), 10.msat, 100, CltvExpiryDelta(144))), + listOf(Bolt11Invoice.TaggedField.ExtraHop(node2, ShortChannelId(2), 20.msat, 150, CltvExpiryDelta(12)), Bolt11Invoice.TaggedField.ExtraHop(node3, ShortChannelId(3), 30.msat, 200, CltvExpiryDelta(24))) ) val expected = PaymentOnion.NodeRelayPayload( TlvStream(