Skip to content

Commit 4b2ffbc

Browse files
robbiehansont-bast
andauthored
Introduce OfferPaymentMetadata.V2 to fix offer description (#816)
We introduce a v2 of our `OfferPaymentMetadata` that stores both the offer description and the payer note (when available). We also switch to seconds instead of milliseconds, encryption instead of signing, and improve the encoded size. Co-authored-by: t-bast <bastien@acinq.fr>
1 parent 6f4cd99 commit 4b2ffbc

File tree

9 files changed

+333
-62
lines changed

9 files changed

+333
-62
lines changed

modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package fr.acinq.lightning
22

33
import co.touchlab.kermit.Logger
44
import fr.acinq.bitcoin.Chain
5-
import fr.acinq.bitcoin.PrivateKey
65
import fr.acinq.bitcoin.PublicKey
76
import fr.acinq.bitcoin.Satoshi
87
import fr.acinq.lightning.Lightning.nodeFee
@@ -267,7 +266,7 @@ data class NodeParams(
267266
*
268267
* @return the default offer and the private key that will sign invoices for this offer.
269268
*/
270-
fun defaultOffer(trampolineNodeId: PublicKey): Pair<OfferTypes.Offer, PrivateKey> = OfferManager.deterministicOffer(chainHash, nodePrivateKey, trampolineNodeId, null, null, null)
269+
fun defaultOffer(trampolineNodeId: PublicKey): OfferTypes.OfferAndKey = OfferManager.deterministicOffer(chainHash, nodePrivateKey, trampolineNodeId, null, null, null)
271270

272271
/**
273272
* Generate a random Bolt 12 offer based on the node's seed and its trampoline node.
@@ -276,7 +275,7 @@ data class NodeParams(
276275
*
277276
* @return a random offer and the private key that will sign invoices for this offer.
278277
*/
279-
fun randomOffer(trampolineNodeId: PublicKey, amount: MilliSatoshi?, description: String?): Pair<OfferTypes.Offer, PrivateKey> {
278+
fun randomOffer(trampolineNodeId: PublicKey, amount: MilliSatoshi?, description: String?): OfferTypes.OfferAndKey {
280279
// We generate a random nonce to ensure that this offer is unique.
281280
val nonce = randomBytes32()
282281
return OfferManager.deterministicOffer(chainHash, nodePrivateKey, trampolineNodeId, amount, description, nonce)

modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -860,7 +860,7 @@ class Peer(
860860
.first()
861861
.let { event -> replyTo.complete(event.address) }
862862
}
863-
peerConnection?.send(DNSAddressRequest(nodeParams.chainHash, nodeParams.defaultOffer(walletParams.trampolineNode.id).first, languageSubtag))
863+
peerConnection?.send(DNSAddressRequest(nodeParams.chainHash, nodeParams.defaultOffer(walletParams.trampolineNode.id).offer, languageSubtag))
864864
return replyTo.await()
865865
}
866866

modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
479479
}
480480
is PaymentOnion.FinalPayload.Blinded -> {
481481
// We encrypted the payment metadata for ourselves in the blinded path we included in the invoice.
482-
return when (val metadata = OfferPaymentMetadata.fromPathId(nodeParams.nodeId, finalPayload.pathId)) {
482+
return when (val metadata = OfferPaymentMetadata.fromPathId(nodeParams.nodePrivateKey, finalPayload.pathId, paymentPart.paymentHash)) {
483483
null -> {
484484
logger.warning { "invalid path_id: ${finalPayload.pathId.toHex()}" }
485485
Either.Left(rejectPaymentPart(privateKey, paymentPart, null, currentBlockHeight))
@@ -503,7 +503,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
503503
logger.warning { "payment with expiry too small: ${paymentPart.htlc.cltvExpiry}, min is ${minFinalCltvExpiry(nodeParams, paymentPart, incomingPayment, currentBlockHeight)}" }
504504
Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight))
505505
}
506-
metadata.createdAtMillis + nodeParams.bolt12InvoiceExpiry.inWholeMilliseconds < currentTimestampMillis() && incomingPayment.parts.isEmpty() && !paysPreviousOnTheFlyFunding -> {
506+
metadata.createdAtSeconds + (metadata.relativeExpirySeconds ?: nodeParams.bolt12InvoiceExpiry.inWholeSeconds) < currentTimestampSeconds() && incomingPayment.parts.isEmpty() && !paysPreviousOnTheFlyFunding -> {
507507
logger.warning { "the invoice is expired" }
508508
Either.Left(rejectPaymentPart(privateKey, paymentPart, incomingPayment, currentBlockHeight))
509509
}

modules/core/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import fr.acinq.lightning.message.OnionMessages
1717
import fr.acinq.lightning.message.OnionMessages.Destination
1818
import fr.acinq.lightning.message.OnionMessages.IntermediateNode
1919
import fr.acinq.lightning.message.OnionMessages.buildMessage
20-
import fr.acinq.lightning.utils.currentTimestampMillis
20+
import fr.acinq.lightning.utils.currentTimestampSeconds
2121
import fr.acinq.lightning.utils.toByteVector
2222
import fr.acinq.lightning.wire.*
2323
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -153,14 +153,19 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
153153
else -> {
154154
val amount = request.requestedAmount
155155
val preimage = randomBytes32()
156-
val truncatedPayerNote = (request.payerNote ?: request.offer.description)?.let {
157-
if (it.length <= 64) {
158-
it
159-
} else {
160-
it.take(63) + ""
161-
}
162-
}
163-
val metadata = OfferPaymentMetadata.V1(request.offer.offerId, amount, preimage, request.payerId, truncatedPayerNote, request.quantity, currentTimestampMillis()).toPathId(nodeParams.nodePrivateKey)
156+
val (truncatedPayerNote, truncatedDescription) = OfferPaymentMetadata.truncateNotes(request.payerNote, request.offer.description)
157+
val expirySeconds = request.offer.expirySeconds ?: nodeParams.bolt12InvoiceExpiry.inWholeSeconds
158+
val metadata = OfferPaymentMetadata.V2(
159+
offerId = request.offer.offerId,
160+
amount = amount,
161+
preimage = preimage,
162+
createdAtSeconds = currentTimestampSeconds(),
163+
relativeExpirySeconds = expirySeconds,
164+
description = truncatedDescription,
165+
payerKey = request.payerId,
166+
payerNote = truncatedPayerNote,
167+
quantity = request.quantity_opt
168+
).toPathId(nodeParams.nodePrivateKey)
164169
val recipientPayload = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.PathId(metadata))).write().toByteVector()
165170
val cltvExpiryDelta = remoteChannelUpdates.maxOfOrNull { it.cltvExpiryDelta } ?: walletParams.invoiceDefaultRoutingFees.cltvExpiryDelta
166171
val paymentInfo = OfferTypes.PaymentInfo(
@@ -191,7 +196,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
191196
).write().toByteVector()
192197
val blindedRoute = RouteBlinding.create(randomKey(), listOf(remoteNodeId, nodeParams.nodeId), listOf(remoteNodePayload, recipientPayload)).route
193198
val path = Bolt12Invoice.Companion.PaymentBlindedContactInfo(OfferTypes.ContactInfo.BlindedPath(blindedRoute), paymentInfo)
194-
val invoice = Bolt12Invoice(request, preimage, blindedPrivateKey, nodeParams.bolt12InvoiceExpiry.inWholeSeconds, nodeParams.features.bolt12Features(), listOf(path))
199+
val invoice = Bolt12Invoice(request, preimage, blindedPrivateKey, expirySeconds, nodeParams.features.bolt12Features(), listOf(path))
195200
val destination = Destination.BlindedPath(replyPath)
196201
when (val invoiceMessage = buildMessage(randomKey(), randomKey(), intermediateNodes(destination), destination, TlvStream(OnionMessagePayloadTlv.Invoice(invoice.records)))) {
197202
is Left -> {
@@ -233,7 +238,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
233238
pathId != null && pathId.size() != 32 -> false
234239
else -> {
235240
val expected = deterministicOffer(nodeParams.chainHash, nodeParams.nodePrivateKey, walletParams.trampolineNode.id, offer.amount, offer.description, pathId?.let { ByteVector32(it) })
236-
expected == Pair(offer, blindedPrivateKey)
241+
expected == OfferTypes.OfferAndKey(offer, blindedPrivateKey)
237242
}
238243
}
239244

@@ -249,7 +254,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
249254
amount: MilliSatoshi?,
250255
description: String?,
251256
pathId: ByteVector32?,
252-
): Pair<OfferTypes.Offer, PrivateKey> {
257+
): OfferTypes.OfferAndKey {
253258
// We generate a deterministic session key based on:
254259
// - a custom tag indicating that this is used in the Bolt 12 context
255260
// - the offer parameters (amount, description and pathId)

0 commit comments

Comments
 (0)