diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt11Invoice.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt11Invoice.kt index a778716e4..2c697c88b 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt11Invoice.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt11Invoice.kt @@ -47,6 +47,8 @@ data class Bolt11Invoice( val routingInfo: List = tags.filterIsInstance() + override val accountable: Boolean = tags.contains(TaggedField.Accountable) + init { val f = features.invoiceFeatures() require(f.hasFeature(Feature.VariableLengthOnion)) { "${Feature.VariableLengthOnion.rfcName} must be supported" } @@ -140,7 +142,8 @@ data class Bolt11Invoice( TaggedField.MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt()), TaggedField.PaymentSecret(paymentSecret), // We remove unknown features which could make the invoice too big. - TaggedField.Features(features.invoiceFeatures().copy(unknown = setOf()).toByteArray().toByteVector()) + TaggedField.Features(features.invoiceFeatures().copy(unknown = setOf()).toByteArray().toByteVector()), + TaggedField.Accountable ) description.left?.let { tags.add(TaggedField.Description(it)) } description.right?.let { tags.add(TaggedField.DescriptionHash(it)) } @@ -198,6 +201,7 @@ data class Bolt11Invoice( TaggedField.Features.tag -> tags.add(kotlin.runCatching { TaggedField.Features.decode(value) }.getOrDefault(TaggedField.InvalidTag(tag, value))) TaggedField.RoutingInfo.tag -> tags.add(kotlin.runCatching { TaggedField.RoutingInfo.decode(value) }.getOrDefault(TaggedField.InvalidTag(tag, value))) TaggedField.NodeId.tag -> tags.add(kotlin.runCatching { TaggedField.NodeId.decode(value) }.getOrDefault(TaggedField.InvalidTag(tag, value))) + TaggedField.Accountable.tag -> tags.add(if (value.isEmpty()) TaggedField.Accountable else TaggedField.InvalidTag(tag, value)) else -> tags.add(TaggedField.UnknownTag(tag, value)) } loop(input.drop(3 + len)) @@ -518,6 +522,12 @@ data class Bolt11Invoice( } } + /** Present if the recipient is willing to be held accountable for the timely resolution of HTLCs. */ + data object Accountable : TaggedField() { + override val tag: Int5 = 31 + override fun encode(): List = emptyList() + } + /** Unknown tag (may or may not be valid) */ data class UnknownTag(override val tag: Int5, val value: List) : TaggedField() { override fun encode(): List = value.toList() diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt index ca736809b..07afe47ec 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/Bolt12Invoice.kt @@ -4,14 +4,12 @@ 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.Features.Companion.invoke import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.utils.currentTimestampSeconds import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.* +import fr.acinq.lightning.wire.OfferTypes.InvoiceAccountable import fr.acinq.lightning.wire.OfferTypes.ContactInfo.BlindedPath import fr.acinq.lightning.wire.OfferTypes.FallbackAddress import fr.acinq.lightning.wire.OfferTypes.InvoiceAmount @@ -50,6 +48,8 @@ data class Bolt12Invoice(val records: TlvStream) : PaymentRequest() val fallbacks: List? = records.get()?.addresses val signature: ByteVector64 = records.get()!!.signature + override val accountable: Boolean = records.get() != null + override fun isExpired(currentTimestampSeconds: Long): Boolean = createdAtSeconds + relativeExpirySeconds <= currentTimestampSeconds // It is assumed that the request is valid for this offer. @@ -112,6 +112,7 @@ data class Bolt12Invoice(val records: TlvStream) : PaymentRequest() val amount = request.amount ?: (request.offer.amount!! * request.quantity) val tlvs: Set = removeSignature(request.records).records + setOfNotNull( InvoicePaths(paths.map { it.route }), + InvoiceAccountable, InvoiceBlindedPay(paths.map { it.paymentInfo }), InvoiceCreatedAt(currentTimestampSeconds()), InvoiceRelativeExpiry(invoiceExpirySeconds), diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentPacket.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentPacket.kt index 63e1d99ec..13ec4dd75 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentPacket.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentPacket.kt @@ -128,7 +128,7 @@ object IncomingPaymentPacket { else -> { // We merge contents from the outer and inner payloads. // We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless). - Either.Right(PaymentOnion.FinalPayload.Standard.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata)) + Either.Right(PaymentOnion.FinalPayload.Standard.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, innerPayload.upgradeAccountability)) } } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt index f04d01a12..08d6a9ba8 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt @@ -166,7 +166,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v payerNote = truncatedPayerNote, quantity = request.quantity_opt ).toPathId(nodeParams.nodePrivateKey) - val recipientPayload = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.PathId(metadata))).write().toByteVector() + val recipientPayload = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.UpgradeAccountability, RouteBlindingEncryptedDataTlv.PathId(metadata))).write().toByteVector() val cltvExpiryDelta = remoteChannelUpdates.maxOfOrNull { it.cltvExpiryDelta } ?: walletParams.invoiceDefaultRoutingFees.cltvExpiryDelta val paymentInfo = OfferTypes.PaymentInfo( feeBase = remoteChannelUpdates.maxOfOrNull { it.feeBaseMsat } ?: walletParams.invoiceDefaultRoutingFees.feeBase, @@ -189,6 +189,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v val pathExpiry = (paymentInfo.cltvExpiryDelta + CltvExpiryDelta(720) + (nodeParams.bolt12InvoiceExpiry.inWholeMinutes.toInt() / 10)).toCltvExpiry(currentBlockHeight.toLong()) val remoteNodePayload = RouteBlindingEncryptedData( TlvStream( + RouteBlindingEncryptedDataTlv.UpgradeAccountability, RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId.WithPublicKey.Wallet(nodeParams.nodeId)), RouteBlindingEncryptedDataTlv.PaymentRelay(cltvExpiryDelta, paymentInfo.feeProportionalMillionths, paymentInfo.feeBase), RouteBlindingEncryptedDataTlv.PaymentConstraints(pathExpiry, paymentInfo.minHtlc) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt index 593889144..f908249ea 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt @@ -46,8 +46,8 @@ object OutgoingPaymentPacket { fun buildPacketToTrampolineRecipient(invoice: Bolt11Invoice, amount: MilliSatoshi, expiry: CltvExpiry, hop: NodeHop): Triple { require(invoice.features.hasFeature(Feature.ExperimentalTrampolinePayment)) { "invoice must support trampoline" } val trampolineOnion = run { - val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, invoice.paymentMetadata) - val trampolinePayload = PaymentOnion.NodeRelayPayload.create(amount, expiry, hop.nextNodeId) + val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, invoice.paymentMetadata, invoice.accountable) + val trampolinePayload = PaymentOnion.NodeRelayPayload.create(amount, expiry, hop.nextNodeId, invoice.accountable) // We may be paying an older version of lightning-kmp that only supports trampoline packets of size 400. buildOnion(listOf(hop.nodeId, hop.nextNodeId), listOf(trampolinePayload, finalPayload), invoice.paymentHash, payloadLength = 400) } @@ -55,7 +55,7 @@ object OutgoingPaymentPacket { val trampolineExpiry = expiry + hop.cltvExpiryDelta // We generate a random secret to avoid leaking the invoice secret to the trampoline node. val trampolinePaymentSecret = Lightning.randomBytes32() - val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, trampolinePaymentSecret, trampolineOnion.packet) + val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, trampolinePaymentSecret, trampolineOnion.packet, invoice.accountable) val paymentOnion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, OnionRoutingPacket.PaymentPacketLength) return Triple(trampolineAmount, trampolineExpiry, paymentOnion) } @@ -70,10 +70,10 @@ object OutgoingPaymentPacket { fun buildPacketToTrampolinePeer(invoice: Bolt11Invoice, amount: MilliSatoshi, expiry: CltvExpiry): Triple { require(invoice.features.hasFeature(Feature.ExperimentalTrampolinePayment)) { "invoice must support trampoline" } val trampolineOnion = run { - val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, invoice.paymentMetadata) + val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, invoice.paymentMetadata, invoice.accountable) buildOnion(listOf(invoice.nodeId), listOf(finalPayload), invoice.paymentHash) } - val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(amount, amount, expiry, invoice.paymentSecret, trampolineOnion.packet) + val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(amount, amount, expiry, invoice.paymentSecret, trampolineOnion.packet, invoice.accountable) val paymentOnion = buildOnion(listOf(invoice.nodeId), listOf(payload), invoice.paymentHash, OnionRoutingPacket.PaymentPacketLength) return Triple(amount, expiry, paymentOnion) } @@ -93,21 +93,21 @@ object OutgoingPaymentPacket { val trampolineOnion = run { // NB: the final payload will never reach the recipient, since the trampoline node 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.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, null) + val dummyFinalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, invoice.paymentSecret, null, invoice.accountable) var routingInfo = invoice.routingInfo - var trampolinePayload = PaymentOnion.RelayToNonTrampolinePayload.create(amount, amount, expiry, hop.nextNodeId, invoice, routingInfo) + var trampolinePayload = PaymentOnion.RelayToNonTrampolinePayload.create(amount, amount, expiry, hop.nextNodeId, invoice, routingInfo, invoice.accountable) var trampolineOnion = buildOnion(listOf(hop.nodeId, hop.nextNodeId), listOf(trampolinePayload, dummyFinalPayload), invoice.paymentHash) // Ensure that this onion can fit inside the outer 1300 bytes onion. The outer onion fields need ~150 bytes and we add some safety margin. while (trampolineOnion.packet.payload.size() > 1000) { routingInfo = routingInfo.dropLast(1) - trampolinePayload = PaymentOnion.RelayToNonTrampolinePayload.create(amount, amount, expiry, hop.nextNodeId, invoice, routingInfo) + trampolinePayload = PaymentOnion.RelayToNonTrampolinePayload.create(amount, amount, expiry, hop.nextNodeId, invoice, routingInfo, invoice.accountable) trampolineOnion = buildOnion(listOf(hop.nodeId, hop.nextNodeId), listOf(trampolinePayload, dummyFinalPayload), invoice.paymentHash) } trampolineOnion } val trampolineAmount = amount + hop.fee(amount) val trampolineExpiry = expiry + hop.cltvExpiryDelta - val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, invoice.paymentSecret, trampolineOnion.packet) + val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, invoice.paymentSecret, trampolineOnion.packet, invoice.accountable) val paymentOnion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, OnionRoutingPacket.PaymentPacketLength) return Triple(trampolineAmount, trampolineExpiry, paymentOnion) } @@ -140,7 +140,7 @@ object OutgoingPaymentPacket { val trampolineExpiry = expiry + hop.cltvExpiryDelta // We generate a random secret to avoid leaking the invoice secret to the trampoline node. val trampolinePaymentSecret = Lightning.randomBytes32() - val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, trampolinePaymentSecret, trampolineOnion.packet) + val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, trampolinePaymentSecret, trampolineOnion.packet, invoice.accountable) val paymentOnion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, OnionRoutingPacket.PaymentPacketLength) return Triple(trampolineAmount, trampolineExpiry, paymentOnion) } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/PaymentRequest.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/PaymentRequest.kt index dbe5ddc4f..635d7b165 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/PaymentRequest.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/PaymentRequest.kt @@ -12,6 +12,7 @@ sealed class PaymentRequest { abstract val paymentHash: ByteVector32 abstract val nodeId: PublicKey abstract val features: Features + abstract val accountable: Boolean abstract fun isExpired(currentTimestampSeconds: Long = currentTimestampSeconds()): Boolean diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt index 6157f8983..34d86326d 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt @@ -441,6 +441,19 @@ object OfferTypes { } } + /** + * By setting this field, we let the payer know that we will resolve the payment quickly once we receive it. + * If we don't, our reputation will be negatively impacted (channel jamming protection). + */ + data object InvoiceAccountable : InvoiceTlv(), TlvValueReader { + override val tag: Long = 161 + override fun write(out: Output) {} + override fun read(input: Input): InvoiceAccountable { + require(input.availableBytes == 0) + return InvoiceAccountable + } + } + data class PaymentInfo( val feeBase: MilliSatoshi, val feeProportionalMillionths: Long, @@ -1030,6 +1043,7 @@ object OfferTypes { InvoiceRequestPayerNote.tag to InvoiceRequestPayerNote as TlvValueReader, // Invoice part InvoicePaths.tag to InvoicePaths as TlvValueReader, + InvoiceAccountable.tag to InvoiceAccountable as TlvValueReader, InvoiceBlindedPay.tag to InvoiceBlindedPay as TlvValueReader, InvoiceCreatedAt.tag to InvoiceCreatedAt as TlvValueReader, InvoiceRelativeExpiry.tag to InvoiceRelativeExpiry as TlvValueReader, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt index 295d36f27..0a6c73008 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt @@ -229,6 +229,16 @@ sealed class OnionPaymentPayloadTlv : Tlv { } } + /** Flag to allow forwarding nodes to set `accountable` in their `update_add_htlc` */ + data object UpgradeAccountability : OnionPaymentPayloadTlv(), TlvValueReader { + override val tag: Long = 19 + override fun write(out: Output) {} + override fun read(input: Input): UpgradeAccountability { + require(input.availableBytes == 0) + return UpgradeAccountability + } + } + } object PaymentOnion { @@ -259,6 +269,7 @@ object PaymentOnion { OnionPaymentPayloadTlv.InvoiceRoutingInfo.tag to OnionPaymentPayloadTlv.InvoiceRoutingInfo.Companion as TlvValueReader, OnionPaymentPayloadTlv.TrampolineOnion.tag to OnionPaymentPayloadTlv.TrampolineOnion.Companion as TlvValueReader, OnionPaymentPayloadTlv.OutgoingBlindedPaths.tag to OnionPaymentPayloadTlv.OutgoingBlindedPaths.Companion as TlvValueReader, + OnionPaymentPayloadTlv.UpgradeAccountability.tag to OnionPaymentPayloadTlv.UpgradeAccountability as TlvValueReader, ) ) @@ -293,6 +304,7 @@ object PaymentOnion { if (total > 0.msat) total else amount } val paymentMetadata = records.get()?.data + val upgradeAccountability = records.get() != null override fun write(out: Output) = tlvSerializer.write(records, out) @@ -315,6 +327,7 @@ object PaymentOnion { expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: ByteVector?, + upgradeAccountability: Boolean, userCustomTlvs: Set = setOf() ): Standard { val tlvs = buildSet { @@ -322,6 +335,9 @@ object PaymentOnion { add(OnionPaymentPayloadTlv.OutgoingCltv(expiry)) add(OnionPaymentPayloadTlv.PaymentData(paymentSecret, amount)) paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) } + if (upgradeAccountability) { + add(OnionPaymentPayloadTlv.UpgradeAccountability) + } } return Standard(TlvStream(tlvs, userCustomTlvs)) } @@ -333,6 +349,7 @@ object PaymentOnion { expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: ByteVector?, + upgradeAccountability: Boolean, additionalTlvs: Set = setOf(), userCustomTlvs: Set = setOf() ): Standard { @@ -341,6 +358,9 @@ object PaymentOnion { add(OnionPaymentPayloadTlv.OutgoingCltv(expiry)) add(OnionPaymentPayloadTlv.PaymentData(paymentSecret, totalAmount)) paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) } + if (upgradeAccountability) { + add(OnionPaymentPayloadTlv.UpgradeAccountability) + } addAll(additionalTlvs) } return Standard(TlvStream(tlvs, userCustomTlvs)) @@ -352,15 +372,19 @@ object PaymentOnion { totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, - trampolinePacket: OnionRoutingPacket + trampolinePacket: OnionRoutingPacket, + upgradeAccountability: Boolean, ): Standard { - val tlvs = TlvStream( - OnionPaymentPayloadTlv.AmountToForward(amount), - OnionPaymentPayloadTlv.OutgoingCltv(expiry), - OnionPaymentPayloadTlv.PaymentData(paymentSecret, totalAmount), - OnionPaymentPayloadTlv.TrampolineOnion(trampolinePacket) - ) - return Standard(tlvs) + val tlvs = buildSet { + add(OnionPaymentPayloadTlv.AmountToForward(amount)) + add(OnionPaymentPayloadTlv.OutgoingCltv(expiry)) + add(OnionPaymentPayloadTlv.PaymentData(paymentSecret, totalAmount)) + add(OnionPaymentPayloadTlv.TrampolineOnion(trampolinePacket)) + if (upgradeAccountability) { + add(OnionPaymentPayloadTlv.UpgradeAccountability) + } + } + return Standard(TlvStream(tlvs)) } } } @@ -417,8 +441,17 @@ object PaymentOnion { } } - fun create(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry): ChannelRelayPayload = - ChannelRelayPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amountToForward), OnionPaymentPayloadTlv.OutgoingCltv(outgoingCltv), OnionPaymentPayloadTlv.OutgoingChannelId(outgoingChannelId))) + fun create(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry, upgradeAccountability: Boolean): ChannelRelayPayload { + val tlvs = buildSet { + add(OnionPaymentPayloadTlv.AmountToForward(amountToForward)) + add(OnionPaymentPayloadTlv.OutgoingCltv(outgoingCltv)) + add(OnionPaymentPayloadTlv.OutgoingChannelId(outgoingChannelId)) + if (upgradeAccountability) { + add(OnionPaymentPayloadTlv.UpgradeAccountability) + } + } + return ChannelRelayPayload(TlvStream(tlvs)) + } } } @@ -461,8 +494,17 @@ object PaymentOnion { } } - fun create(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey) = - NodeRelayPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amount), OnionPaymentPayloadTlv.OutgoingCltv(expiry), OnionPaymentPayloadTlv.OutgoingNodeId(nextNodeId))) + fun create(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey, upgradeAccountability: Boolean): NodeRelayPayload { + val tlvs = buildSet { + add(OnionPaymentPayloadTlv.AmountToForward(amount)) + add(OnionPaymentPayloadTlv.OutgoingCltv(expiry)) + add(OnionPaymentPayloadTlv.OutgoingNodeId(nextNodeId)) + if (upgradeAccountability) { + add(OnionPaymentPayloadTlv.UpgradeAccountability) + } + } + return NodeRelayPayload(TlvStream(tlvs)) + } } } @@ -504,7 +546,7 @@ object PaymentOnion { } } - fun create(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice, routingInfo: List): RelayToNonTrampolinePayload = + fun create(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice, routingInfo: List, upgradeAccountability: Boolean): RelayToNonTrampolinePayload = RelayToNonTrampolinePayload( TlvStream( buildSet { @@ -515,6 +557,9 @@ object PaymentOnion { invoice.paymentMetadata?.let { add(OnionPaymentPayloadTlv.PaymentMetadata(it)) } add(OnionPaymentPayloadTlv.InvoiceFeatures(invoice.features.toByteArray().toByteVector())) add(OnionPaymentPayloadTlv.InvoiceRoutingInfo(routingInfo.map { it.hints })) + if (upgradeAccountability) { + add(OnionPaymentPayloadTlv.UpgradeAccountability) + } } ) ) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt index 99c989f62..49648d03c 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt @@ -32,6 +32,16 @@ sealed class RouteBlindingEncryptedDataTlv : Tlv { } } + /** Flag to allow forwarding nodes to set `accountable` in their `update_add_htlc` */ + data object UpgradeAccountability : RouteBlindingEncryptedDataTlv(), TlvValueReader { + override val tag: Long = 3 + override fun write(out: Output) {} + override fun read(input: Input): UpgradeAccountability { + require(input.availableBytes == 0) + return UpgradeAccountability + } + } + /** Id of the next node. */ data class OutgoingNodeId(val nodeId: EncodedNodeId) : RouteBlindingEncryptedDataTlv() { override val tag: Long get() = OutgoingNodeId.tag @@ -144,6 +154,7 @@ data class RouteBlindingEncryptedData(val records: TlvStream, RouteBlindingEncryptedDataTlv.OutgoingChannelId.tag to RouteBlindingEncryptedDataTlv.OutgoingChannelId as TlvValueReader, + RouteBlindingEncryptedDataTlv.UpgradeAccountability.tag to RouteBlindingEncryptedDataTlv.UpgradeAccountability as TlvValueReader, RouteBlindingEncryptedDataTlv.OutgoingNodeId.tag to RouteBlindingEncryptedDataTlv.OutgoingNodeId as TlvValueReader, RouteBlindingEncryptedDataTlv.PathId.tag to RouteBlindingEncryptedDataTlv.PathId as TlvValueReader, RouteBlindingEncryptedDataTlv.NextPathKey.tag to RouteBlindingEncryptedDataTlv.NextPathKey as TlvValueReader, diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index 22cdf29fc..16217872e 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -546,7 +546,7 @@ object TestsHelper { fun makeCmdAdd(amount: MilliSatoshi, destination: PublicKey, currentBlockHeight: Long, paymentPreimage: ByteVector32 = randomBytes32(), paymentId: UUID = UUID.randomUUID()): Pair { val paymentHash = Crypto.sha256(paymentPreimage).toByteVector32() val expiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val payload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, randomBytes32(), null) + val payload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(amount, expiry, randomBytes32(), null, upgradeAccountability = true) val onion = OutgoingPaymentPacket.buildOnion(listOf(destination), listOf(payload), paymentHash, OnionRoutingPacket.PaymentPacketLength).packet val cmd = ChannelCommand.Htlc.Add(amount, paymentHash, expiry, onion, paymentId, commit = false) return Pair(paymentPreimage, cmd) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt11InvoiceTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt11InvoiceTestsCommon.kt index 9e47fc4fe..fffa82e84 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt11InvoiceTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt11InvoiceTestsCommon.kt @@ -610,6 +610,18 @@ class Bolt11InvoiceTestsCommon : LightningTestSuite() { assertEquals(fallbackAddress1, invoice2.fallbackAddress) } + @Test + fun `accountable invoice`() { + val paymentHash = ByteVector32("a6d4fb71fce77dab6e4e7b674ac3b2b8994c0911a799cab0f06eb6b27e046cca") + val paymentSecret = ByteVector32("25c149bc0bbe1d1a4d01d5048c32a0839059174b07ab2edc8063ddbd22200afa") + val features = Features(Feature.VariableLengthOnion to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory) + val invoice = Bolt11Invoice.create(Chain.Mainnet, 60000.msat, paymentHash, priv, Either.Left("accountable invoice"), CltvExpiryDelta(18), features, paymentSecret = paymentSecret, timestampSeconds = 1767864238L) + assertTrue(invoice.accountable) + val expected = "lnbc600n1p547aawpp55m20ku0uua76kmjw0dn54sajhzv5czg357vu4v8sd6mtylsydn9qcqpjsp5yhq5n0qthcw35ngp65zgcv4qswg9j96tq74jahyqv0wm6g3qptaq9qrsgqlqqdqlv93kxmm4de6xzcnvv5sxjmnkda5kxeg939n82lq9kcjzpxjv3d8ull0u5fe8v389sjq9457yljmfuml33wruy0wcl78vp6hqtrvpl7kg2wsga2zdfwx29937hecvdnu7wzpnrgqre2xnf" + assertEquals(expected, invoice.write()) + assertEquals(invoice, Bolt11Invoice.read(expected).get()) + } + companion object { fun createInvoiceUnsafe( amount: MilliSatoshi? = null, diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt index 6ce95cf75..e66ae634d 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt @@ -534,10 +534,19 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { } @Test - fun `invoice with non-minimally encoded feature bits`(){ + fun `invoice with non-minimally encoded feature bits`() { val encodedInvoice = "lni1qqsyzre2s0lc77w5h33ck6540xxsyjehjl66f9tfp83w85zcxqyhltczyqrzymjxzydqkkw24ufxqslttwlj3s608f0rx2slc7etw0833zgs7zqyqh67zqq2qqgwsqktzd0na8g54f2r8secsaemc7ww2d6spl397celwcv20egnau2z8gp83d0dg7gvtkkvklnqlvp0erhq9nh9928rexerg578wnyew6dj6xczq2nqtavvd94k7jq2slng76uk560g6qeu38ru2gjjtdd4w9jxfqcc5qpnvvduearw4k75xdsgrc9ntzs274hwumtk5zwlrcr8yzwn8q0ry40f6lcmarq2nqkz9j2anajrlpchwwfguypms9x0uptvcsspwzjp3vg8srqx27crkqe8v9nzqaktzwwy5szk0rsq9sq7vhqncvv63mseqsx9lzmjraxhfnhc6f9tgnm05v7x0s4dhzwac9gruy44n9yht645cd4jzcssyjvcf2ptqztsenmzyw0e6kpx209mmmpal9ptutxpeygerepwh5rc2qsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzr6jqsae4jsq2spsyqqqtqss9g4l2s06jx69u2vtvezfmh07puh8pzhp76yddr7yvjpt2q38puqx5r7sgacrnpvghfhfzdzm9rertx4egjnarr2plwp26yfzcnv4ef536h9nu8lq9xyejhphnyv97axrqwr982vvedhfzj3cn5uhdymxwejfh55p2putqvpeskyt5m53x3dj3u34n2u5ff7334qlhq4dzy3vfk2u56gatje7rlsqgllx5cs3433fgn37scpz5ysn7df4tcfvgw5hgn998qut5l63vvmlv85xj4gj9rs6ja6gj45ddfjvwrcq9qthepk3xtpy4x8tsmmaqhas3v8k6chxp4ds8367lgw3q4mtpm5zmlr84tx4xpshtaxa0es0kcjuah80xt23pm08qprase5e2euq8ndvymuzcdznh78qyg28lw65wve2fpphd5zpwy4v3gfpa245dgtmqkp34gg8s4tfxytnx5vxhclwzmpzdy80jlfyznklk9t0karg42yvxqey68py3t0yg5rew5jke2sr6l5akw3r4x4cyp5f9ty27yjqtsn5ucqywkk84sxudl89xdxw34kvvtq67pk64r3kmyzz5dum0c66sjh7a5ylr6u38ycdmdq5rm7pp5m87rmsg7ntkqr4dcateeafrchaw085my236hxg47745nsrdtmjvnhy4a9ppd95g5m3u40wa0pcnmlhcm99xd0flh0484vht6ysx5cg5nmjxzaqsqv33sgptsrgmfuqgwjuvw5v58k379638h6hda8tqvpk4aexfmj27jsskj6y2dc72hhwhsufalmudjjnxh5lmh6n6kt4azgqg7en2fg446vmtj2zgncc9wv4sa8zhyxm60zadqlf664d8mhdx6g5g6cls2glkqdmayuvypt7fuljtswlmz4w5e8nkkpzr8m6txz7gzvfcexj9dmdhuhsx35lnwnmzm52vq2wgr49g25dwk4jlh0n2yq6yufpewngg7llkgxwqpr5nlruajj55sel09axp2tmkhaf2hkh2lsjyth098l2r2kfg7u9440ymwswpwd20j9zdp562ejm0yy0x68q4knmd6a6g4nz0a2nm3842yw4pdx8udqggqkxa03jwmrzzuzwp2mn6az3exhunlqcpmphsks3cur22l3hvzn74vqy0kf70r6hd5cy2va94czl9g594856j287cefqej8qlre5ewyc5l02wtsx0hcjr4jhup6z4rj46lmrylsr034r5w2csnsgcy83yz848lafh5wue9aue8grnpvghfhfzdzm9rertx4egjnarr2plwp26yfzcnv4ef536h9nu8lq86u0a3w8zcxwy9hj9gvdwv8fhahpdauyzmuegpkefl3xc798mft7qvpeskyt5m53x3dj3u34n2u5ff7334qlhq4dzy3vfk2u56gatje7rlsqg0xlmw039msmmqtt4jqkgqts08ervu9dsx05qwzr67dazwklna9yjzdker5mhmeghxde2jlu5gvl4wrshvrg6x6a0j7hqsgpcc3ngm0ucvftuq6k8q0tpgxknk3d3t8nc9p9frafrfndz788hkaut704urzsj06t45qy8qk5hewf9p3sej3m2xrwyk6ny5hg8t24aq50a7re8evssrd0nmtrpjttuj04nlhs8ygteqepyc6sg5lsdajrc63xjp26j7surx83vx5u4326qfk6vw0sqhme6cw9247ef75ymtz4mp3esduvl07ykrnzzre3aq5jgqzrzcj59yjdcvp38nq7uvdqwmnhvy0h7t9062znl8ly02k9d02tyxev6mf6we8ztfjrdu73wc6gctxg5lmgj4a8v8z9lzqdfvlsmcwzyznagl929pqqqqyfjqqqqqpszqcqqqqqqqqqq3xqqqqqqz0490jqqqgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpzvsqqqqqvqsxqqqqqqqqqqyfsqqqqqqnaftusqqzqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgnyqqqqqrqypsqqqqqqqqqpzvqqqqqqp4rw2vqqqsqqqqqqqqqqqqqqqqqqqqqqqqqzjqg6zm7ju2sgrmk0u67xstmskz34gfjfnjfxwvjltp3jsrd8rn40s7pgk8tzxwt64qgwu6egqtqggzfxvy4q4sp9cvea3z88uatqn98jaaas7ljs479nqujyv3usht6pu0qs8wdac52sykqfjnxg0xhva4fcv00hr4tqzjwkjnkayykkm9dnr97ladr5jjjx4xyjtun7ucye660akfv4nl9tupwnyemp0sasfxapvcw" val invoice = Bolt12Invoice.fromString(encodedInvoice).get() assertTrue(invoice.checkSignature()) assertEquals(invoice.amount, 1000000000.msat) } + + @Test + fun `accountable invoice`() { + val encodedInvoice = "lni1qqswg5pzt6anzaxaypy8y46zknl8zn2a2jqyzrp74gtfm4lp6utpkzcgqsdjuqsqpgvk66twd9kkzmpqdanxvetjypmkjargypsk6mm4de6pvggrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet64qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqpfqgxewqmf9sggr86209t74drgj3upwe6zy449q58wl9f8r5z97ktd6zxelzy6tq5t6pgqrcj9vjlsf709m46e4kq425mg89dthy6zp5dxjt9fp2l9v5c9pet6sxsjzfuhrkcdtmd0xmls7h7w0206ccw75976mtrup24ymhnv590w3qyphwz4l5ypxryqfu620r8ernyfflzf2s5qp0y2553u96r0eaj5rvgsq8gfus2l0sqkesrgr3y8zkjqcsalfvvdg7pjas6pzf3l7lwmg0vnyg6gugzv5p3c4gs2puwyc6eaxtj83yf6cnghr9dj64aapqz3pcqqqqqqsqqqqqgqqxqqqqqqqqqqqqsqqqqqqqqqqqpgqqzjqg62lst26vqsp9j5zqt9su7c9ydn8sazscv767mf09s7tvyezyyexu8qmfxkf0lwhadeq4gzpktsx62czzq7y3tyhuz0newawkdds924x6pet2aexssdrf5je2g2het9xpgw27hcypvvuuadnlmdfjx9kgkq4glat5uzek5589da30zrfef3a6h3mxfgyw3e8n48tptu6pes0znnw0lk3u0tgjluqxz7y0rc3g63hwfffnfgs" + val invoice = Bolt12Invoice.fromString(encodedInvoice).get() + assertTrue(invoice.checkSignature()) + assertTrue(invoice.accountable) + assertEquals(456001234.msat, invoice.amount) + } } \ No newline at end of file diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt index 7df3e7ca0..4dfd9f0cb 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt @@ -347,7 +347,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { associatedData = incomingPayment.paymentHash, ).packet assertTrue(trampolineOnion.payload.size() < 500) - val finalPayload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolinePayload.amount, trampolinePayload.totalAmount, trampolinePayload.expiry, randomBytes32(), trampolineOnion) + val finalPayload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolinePayload.amount, trampolinePayload.totalAmount, trampolinePayload.expiry, randomBytes32(), trampolineOnion, upgradeAccountability = true) makeWillAddHtlc(paymentHandler, incomingPayment.paymentHash, finalPayload) } val result = paymentHandler.process(willAddHtlc, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, TestConstants.fundingRates) @@ -399,7 +399,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { associatedData = incomingPayment.paymentHash, ).packet assertTrue(trampolineOnion.payload.size() < 500) - val finalPayload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolinePayload.amount, trampolinePayload.totalAmount, trampolinePayload.expiry, randomBytes32(), trampolineOnion) + val finalPayload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolinePayload.amount, trampolinePayload.totalAmount, trampolinePayload.expiry, randomBytes32(), trampolineOnion, upgradeAccountability = true) makeWillAddHtlc(paymentHandler, incomingPayment.paymentHash, finalPayload) } val result = paymentHandler.process(willAddHtlc, Features.empty, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw, TestConstants.fundingRates) @@ -1950,7 +1950,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { currentBlockHeight: Int = TestConstants.defaultBlockHeight ): PaymentOnion.FinalPayload.Standard { val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight.toLong()) - return PaymentOnion.FinalPayload.Standard.createMultiPartPayload(amount, totalAmount, expiry, paymentSecret, null) + return PaymentOnion.FinalPayload.Standard.createMultiPartPayload(amount, totalAmount, expiry, paymentSecret, null, upgradeAccountability = true) } private fun makeBlindedPayload( diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt index 43a30f4fb..7fd4b85eb 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt @@ -88,7 +88,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { // Wallets don't need to create channel routes, but it's useful to test the end-to-end flow. fun encryptChannelRelay(paymentHash: ByteVector32, hops: List, finalPayload: PaymentOnion.FinalPayload): Triple { val (firstAmount, firstExpiry, payloads) = hops.drop(1).reversed().fold(Triple(finalPayload.amount, finalPayload.expiry, listOf(finalPayload))) { (amount, expiry, payloads), hop -> - val payload = PaymentOnion.ChannelRelayPayload.create(hop.lastUpdate.shortChannelId, amount, expiry) + val payload = PaymentOnion.ChannelRelayPayload.create(hop.lastUpdate.shortChannelId, amount, expiry, upgradeAccountability = true) Triple(amount + hop.fee(amount), expiry + hop.cltvExpiryDelta, listOf(payload) + payloads) } val onion = OutgoingPaymentPacket.buildOnion(hops.map { it.nextNodeId }, payloads, paymentHash, OnionRoutingPacket.PaymentPacketLength) @@ -196,7 +196,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { // C forwards the trampoline payment to D over its direct channel. val (amountD, expiryD, onionD) = run { - val payloadD = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(innerC.amountToForward, innerC.amountToForward, innerC.outgoingCltv, randomBytes32(), packetD) + val payloadD = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(innerC.amountToForward, innerC.amountToForward, innerC.outgoingCltv, randomBytes32(), packetD, upgradeAccountability = true) encryptChannelRelay(paymentHash, listOf(ChannelHop(c, d, channelUpdateCD)), payloadD) } assertEquals(amountCD, amountD) @@ -208,7 +208,8 @@ class PaymentPacketTestsCommon : LightningTestSuite() { OnionPaymentPayloadTlv.AmountToForward(finalAmount), OnionPaymentPayloadTlv.OutgoingCltv(finalExpiry), OnionPaymentPayloadTlv.PaymentData(paymentSecret, finalAmount), - OnionPaymentPayloadTlv.PaymentMetadata(paymentMetadata) + OnionPaymentPayloadTlv.PaymentMetadata(paymentMetadata), + OnionPaymentPayloadTlv.UpgradeAccountability, ) ) assertEquals(payloadD, expectedFinalPayload) @@ -236,7 +237,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { // B forwards the trampoline payment to D over an indirect channel route. val (amountC, expiryC, onionC) = run { - val payloadD = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(innerB.amountToForward, innerB.amountToForward, innerB.outgoingCltv, randomBytes32(), packetC) + val payloadD = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(innerB.amountToForward, innerB.amountToForward, innerB.outgoingCltv, randomBytes32(), packetC, upgradeAccountability = true) encryptChannelRelay(paymentHash, listOf(ChannelHop(b, c, channelUpdateBC), ChannelHop(c, d, channelUpdateCD)), payloadD) } assertEquals(amountBC, amountC) @@ -252,7 +253,8 @@ class PaymentPacketTestsCommon : LightningTestSuite() { OnionPaymentPayloadTlv.AmountToForward(finalAmount), OnionPaymentPayloadTlv.OutgoingCltv(finalExpiry), OnionPaymentPayloadTlv.PaymentData(paymentSecret, finalAmount), - OnionPaymentPayloadTlv.PaymentMetadata(paymentMetadata) + OnionPaymentPayloadTlv.PaymentMetadata(paymentMetadata), + OnionPaymentPayloadTlv.UpgradeAccountability, ) ) assertEquals(payloadD, expectedFinalPayload) @@ -287,7 +289,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { // B forwards the trampoline payment to D over an indirect channel route. val (amountC, expiryC, onionC) = run { - val payloadD = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(innerB.amountToForward, innerB.outgoingCltv, innerB.paymentSecret, innerB.paymentMetadata) + val payloadD = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(innerB.amountToForward, innerB.outgoingCltv, innerB.paymentSecret, innerB.paymentMetadata, upgradeAccountability = true) encryptChannelRelay(paymentHash, listOf(ChannelHop(b, c, channelUpdateBC), ChannelHop(c, d, channelUpdateCD)), payloadD) } assertEquals(amountBC, amountC) @@ -304,6 +306,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { OnionPaymentPayloadTlv.OutgoingCltv(finalExpiry), OnionPaymentPayloadTlv.PaymentData(paymentSecret, finalAmount), OnionPaymentPayloadTlv.PaymentMetadata(paymentMetadata), + OnionPaymentPayloadTlv.UpgradeAccountability, ) ) assertEquals(payloadD, expectedFinalPayload) @@ -389,7 +392,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { @Test fun `receive a channel payment`() { // c -> d - val finalPayload = PaymentOnion.FinalPayload.Standard.createMultiPartPayload(finalAmount, finalAmount * 1.5, finalExpiry, paymentSecret, paymentMetadata) + val finalPayload = PaymentOnion.FinalPayload.Standard.createMultiPartPayload(finalAmount, finalAmount * 1.5, finalExpiry, paymentSecret, paymentMetadata, upgradeAccountability = true) val (firstAmount, firstExpiry, onion) = encryptChannelRelay(paymentHash, listOf(ChannelHop(c, d, channelUpdateCD)), finalPayload) val addD = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val payloadD = IncomingPaymentPacket.decrypt(addD, privD).right!! @@ -413,7 +416,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { val (firstAmount, firstExpiry, onion) = encryptChannelRelay( paymentHash, listOf(ChannelHop(c, d, channelUpdateCD)), - PaymentOnion.FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, null) + PaymentOnion.FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, null, upgradeAccountability = true) ) val addD = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reversed())) val failure = IncomingPaymentPacket.decrypt(addD, privD) @@ -429,7 +432,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { val (_, innerC, packetD) = decryptRelayToTrampoline(addC, privC) // C modifies the trampoline onion before forwarding the trampoline payment to D. val (amountD, expiryD, onionD) = run { - val payloadD = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(innerC.amountToForward, innerC.amountToForward, innerC.outgoingCltv, randomBytes32(), packetD.copy(payload = packetD.payload.reversed())) + val payloadD = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(innerC.amountToForward, innerC.amountToForward, innerC.outgoingCltv, randomBytes32(), packetD.copy(payload = packetD.payload.reversed()), upgradeAccountability = true) encryptChannelRelay(paymentHash, listOf(ChannelHop(c, d, channelUpdateCD)), payloadD) } val addD = UpdateAddHtlc(randomBytes32(), 2, amountD, paymentHash, expiryD, onionD.packet) @@ -443,7 +446,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { val (firstAmount, firstExpiry, onion) = encryptChannelRelay( paymentHash.reversed(), listOf(ChannelHop(c, d, channelUpdateCD)), - PaymentOnion.FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, paymentMetadata) + PaymentOnion.FinalPayload.Standard.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, paymentMetadata, upgradeAccountability = true) ) val addD = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val failure = IncomingPaymentPacket.decrypt(addD, privD) @@ -480,7 +483,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { val addC = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val (_, innerC, packetD) = decryptRelayToTrampoline(addC, privC) val (amountD, expiryD, onionD) = run { - val payloadD = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(innerC.amountToForward, innerC.amountToForward, innerC.outgoingCltv, randomBytes32(), packetD) + val payloadD = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(innerC.amountToForward, innerC.amountToForward, innerC.outgoingCltv, randomBytes32(), packetD, upgradeAccountability = true) encryptChannelRelay(paymentHash, listOf(ChannelHop(c, d, channelUpdateCD)), payloadD) } val addD = UpdateAddHtlc(randomBytes32(), 2, amountD - 100.msat, paymentHash, expiryD, onionD.packet) @@ -495,7 +498,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { val addC = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val (_, innerC, packetD) = decryptRelayToTrampoline(addC, privC) val (amountD, expiryD, onionD) = run { - val payloadD = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(innerC.amountToForward, innerC.amountToForward, innerC.outgoingCltv, randomBytes32(), packetD) + val payloadD = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(innerC.amountToForward, innerC.amountToForward, innerC.outgoingCltv, randomBytes32(), packetD, upgradeAccountability = true) encryptChannelRelay(paymentHash, listOf(ChannelHop(c, d, channelUpdateCD)), payloadD) } val addD = UpdateAddHtlc(randomBytes32(), 2, amountD, paymentHash, expiryD - CltvExpiryDelta(12), onionD.packet) @@ -523,7 +526,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() { fun `build htlc failure onion`() { // B sends a payment B -> C -> D. val (amountC, expiryC, onionC) = run { - val payloadD = PaymentOnion.FinalPayload.Standard.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, paymentMetadata = null) + val payloadD = PaymentOnion.FinalPayload.Standard.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, paymentMetadata = null, upgradeAccountability = true) encryptChannelRelay(paymentHash, listOf(ChannelHop(b, c, channelUpdateBC), ChannelHop(c, d, channelUpdateCD)), payloadD) } assertEquals(amountBC, amountC) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt index 01404783c..0c7c80f18 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/PaymentOnionTestsCommon.kt @@ -36,9 +36,10 @@ class PaymentOnionTestsCommon : LightningTestSuite() { @Test fun `encode - decode channel relay per-hop payload`() { val testCases = mapOf( - PaymentOnion.ChannelRelayPayload.create(ShortChannelId(0), MilliSatoshi(0), CltvExpiry(0)) to Hex.decode("0e 0200 0400 06080000000000000000"), - PaymentOnion.ChannelRelayPayload.create(ShortChannelId(42), MilliSatoshi(142000), CltvExpiry(500000)) to Hex.decode("14 0203022ab0 040307a120 0608000000000000002a"), - PaymentOnion.ChannelRelayPayload.create(ShortChannelId(561), MilliSatoshi(1105), CltvExpiry(1729)) to Hex.decode("12 02020451 040206c1 06080000000000000231") + PaymentOnion.ChannelRelayPayload.create(ShortChannelId(0), MilliSatoshi(0), CltvExpiry(0), upgradeAccountability = false) to Hex.decode("0e 0200 0400 06080000000000000000"), + PaymentOnion.ChannelRelayPayload.create(ShortChannelId(42), MilliSatoshi(142000), CltvExpiry(500000), upgradeAccountability = false) to Hex.decode("14 0203022ab0 040307a120 0608000000000000002a"), + PaymentOnion.ChannelRelayPayload.create(ShortChannelId(561), MilliSatoshi(1105), CltvExpiry(1729), upgradeAccountability = false) to Hex.decode("12 02020451 040206c1 06080000000000000231"), + PaymentOnion.ChannelRelayPayload.create(ShortChannelId(97), MilliSatoshi(9632), CltvExpiry(9800), upgradeAccountability = true) to Hex.decode("14 020225a0 04022648 06080000000000000061 1300"), ) testCases.forEach { @@ -162,7 +163,13 @@ class PaymentOnionTestsCommon : LightningTestSuite() { ) ) to Hex.decode( "fd0203 02020231 04012a 0820ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff fe00010234fd01d20002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619cff34152f3a36e52ca94e74927203a560392b9cc7ce3c45809c6be52166c24a595716880f95f178bf5b30ca63141f74db6e92795c6130877cfdac3d4bd3087ee73c65d627ddd709112a848cc99e303f3706509aa43ba7c8a88cba175fccf9a8f5016ef06d3b935dbb15196d7ce16dc1a7157845566901d7b2197e52cab4ce487014b14816e5805f9fcacb4f8f88b8ff176f1b94f6ce6b00bc43221130c17d20ef629db7c5f7eafaa166578c720619561dd14b3277db557ec7dcdb793771aef0f2f667cfdbeae3ac8d331c5994779dffb31e5fc0dbdedc0c592ca6d21c18e47fe3528d6975c19517d7e2ea8c5391cf17d0fe30c80913ed887234ccb48808f7ef9425bcd815c3b586210979e3bb286ef2851bf9ce04e28c40a203df98fd648d2f1936fd2f1def0e77eecb277229b4b682322371c0a1dbfcd723a991993df8cc1f2696b84b055b40a1792a29f710295a18fbd351b0f3ff34cd13941131b8278ba79303c89117120eea691738a9954908195143b039dbeed98f26a92585f3d15cf742c953799d3272e0545e9b744be9d3b4cbb079bfc4b35190eee9f59a1d7b41ba2f773179f322dafb4b1af900c289ebd6c" - ) + ), + TlvStream( + OnionPaymentPayloadTlv.AmountToForward(561.msat), + OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(42)), + OnionPaymentPayloadTlv.PaymentData(ByteVector32("eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1099511627775L.msat), + OnionPaymentPayloadTlv.UpgradeAccountability, + ) to Hex.decode("30 02020231 04012a 0825eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619ffffffffff 1300"), ) testCases.forEach { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/RouteBlindingTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/RouteBlindingTestsCommon.kt index 094d8786e..b9b4b5028 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/RouteBlindingTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/RouteBlindingTestsCommon.kt @@ -2,8 +2,13 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.PublicKey +import fr.acinq.lightning.CltvExpiry +import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.EncodedNodeId +import fr.acinq.lightning.Features +import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.utils.msat import fr.acinq.lightning.wire.RouteBlindingEncryptedDataTlv.* import kotlin.test.Test import kotlin.test.assertEquals @@ -57,4 +62,44 @@ class RouteBlindingTestsCommon : LightningTestSuite() { assertEquals(encoded, ByteVector(data.write())) } } + + @Test + fun `decode payment onion route blinding data for accountable invoice`() { + val payloads = mapOf( + ByteVector("01200000000000000000000000000000000000000000000000000000000000000000 02080000000000000001 0300 0a080032000000002710 0c05000b724632 0e00") to RouteBlindingEncryptedData(TlvStream( + Padding(ByteVector("0000000000000000000000000000000000000000000000000000000000000000")), + OutgoingChannelId(ShortChannelId(1)), + UpgradeAccountability, + PaymentRelay(CltvExpiryDelta(50), 0, 10000.msat), + PaymentConstraints(CltvExpiry(750150), 50.msat), + AllowedFeatures(Features.empty))), + ByteVector("02080000000000000002 0300 0821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 0a07004b0000009664 0c05000b721432 0e00") to RouteBlindingEncryptedData(TlvStream( + OutgoingChannelId(ShortChannelId(2)), + UpgradeAccountability, + NextPathKey(PublicKey(ByteVector("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))), + PaymentRelay(CltvExpiryDelta(75), 150, 100.msat), + PaymentConstraints(CltvExpiry(750100), 50.msat), + AllowedFeatures(Features.empty))), + ByteVector("012200000000000000000000000000000000000000000000000000000000000000000000 02080000000000000003 0300 0a06001900000064 0c05000b71c932 0e00") to RouteBlindingEncryptedData(TlvStream( + Padding(ByteVector("00000000000000000000000000000000000000000000000000000000000000000000")), + OutgoingChannelId(ShortChannelId(3)), + UpgradeAccountability, + PaymentRelay(CltvExpiryDelta(25), 100, 0.msat), + PaymentConstraints(CltvExpiry(750025), 50.msat), + AllowedFeatures(Features.empty))), + ByteVector("011c00000000000000000000000000000000000000000000000000000000 0300 0616c9cf92f45ade68345bc20ae672e2012f4af487ed4415 0c05000b71b032 0e00") to RouteBlindingEncryptedData(TlvStream( + Padding(ByteVector("00000000000000000000000000000000000000000000000000000000")), + UpgradeAccountability, + PathId(ByteVector("c9cf92f45ade68345bc20ae672e2012f4af487ed4415")), + PaymentConstraints(CltvExpiry(750000), 50.msat), + AllowedFeatures(Features.empty))), + ) + + for (payload in payloads) { + val encoded = payload.key + val data = payload.value + assertEquals(data, RouteBlindingEncryptedData.read(encoded.toByteArray()).right) + assertEquals(encoded, ByteVector(data.write())) + } + } } \ No newline at end of file