From e1b162a5b06156182d18e218e96822a59709ac17 Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 21 Aug 2024 17:35:07 +0200 Subject: [PATCH] Include `min_final_cltv_expiry_delta` in blinded paths If a payer uses the current block height as the expiry for the payments they send to us, we will reject it because we need to have a few blocks to potentially force-close and get our funds back safely. With blinded paths, we don't need to rely on payers to add a safety margin: we can directly encode it in the blinded path's total expiry delta. --- .../kotlin/fr/acinq/lightning/payment/OfferManager.kt | 9 +++++++-- .../acinq/lightning/payment/OfferManagerTestsCommon.kt | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt index 1d0aa5b2d..030f113f3 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/OfferManager.kt @@ -159,10 +159,15 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v } val pathId = OfferPaymentMetadata.V1(ByteVector32(decrypted.pathId), amount, preimage, request.payerId, truncatedPayerNote, request.quantity, currentTimestampMillis()).toPathId(nodeParams.nodePrivateKey) val recipientPayload = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.PathId(pathId))).write().toByteVector() + val cltvExpiryDelta = remoteChannelUpdates.maxOfOrNull { it.cltvExpiryDelta } ?: walletParams.invoiceDefaultRoutingFees.cltvExpiryDelta val paymentInfo = OfferTypes.PaymentInfo( feeBase = remoteChannelUpdates.maxOfOrNull { it.feeBaseMsat } ?: walletParams.invoiceDefaultRoutingFees.feeBase, feeProportionalMillionths = remoteChannelUpdates.maxOfOrNull { it.feeProportionalMillionths } ?: walletParams.invoiceDefaultRoutingFees.feeProportional, - cltvExpiryDelta = remoteChannelUpdates.maxOfOrNull { it.cltvExpiryDelta } ?: walletParams.invoiceDefaultRoutingFees.cltvExpiryDelta, + // We include our min_final_cltv_expiry_delta in the path, but we *don't* include it in the payment_relay field + // for our trampoline node (below). This ensures that we will receive payments with at least this final expiry delta. + // This ensures that even when payers haven't received the latest block(s) or don't include a safety margin in the + // expiry they use, we can still safely receive their payment. + cltvExpiryDelta = cltvExpiryDelta + nodeParams.minFinalCltvExpiryDelta, minHtlc = remoteChannelUpdates.minOfOrNull { it.htlcMinimumMsat } ?: 1.msat, maxHtlc = amount, allowedFeatures = Features.empty @@ -170,7 +175,7 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v val remoteNodePayload = RouteBlindingEncryptedData( TlvStream( RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId.WithPublicKey.Wallet(nodeParams.nodeId)), - RouteBlindingEncryptedDataTlv.PaymentRelay(paymentInfo.cltvExpiryDelta, paymentInfo.feeProportionalMillionths, paymentInfo.feeBase), + RouteBlindingEncryptedDataTlv.PaymentRelay(cltvExpiryDelta, paymentInfo.feeProportionalMillionths, paymentInfo.feeBase), RouteBlindingEncryptedDataTlv.PaymentConstraints((paymentInfo.cltvExpiryDelta + nodeParams.maxFinalCltvExpiryDelta).toCltvExpiry(currentBlockHeight.toLong()), paymentInfo.minHtlc) ) ).write().toByteVector() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/OfferManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/OfferManagerTestsCommon.kt index ffdbeebd7..974664394 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/OfferManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/OfferManagerTestsCommon.kt @@ -85,6 +85,10 @@ class OfferManagerTestsCommon : LightningTestSuite() { assertIs(payInvoice) assertEquals(OfferInvoiceReceived(payOffer, payInvoice.invoice), bobOfferManager.eventsFlow.first()) assertEquals(payOffer, payInvoice.payOffer) + assertEquals(1, payInvoice.invoice.blindedPaths.size) + val path = payInvoice.invoice.blindedPaths.first() + assertEquals(EncodedNodeId(aliceTrampolineKey.publicKey()), path.route.route.introductionNodeId) + assertEquals(aliceOfferManager.nodeParams.expiryDeltaBlocks + aliceOfferManager.nodeParams.minFinalCltvExpiryDelta, path.paymentInfo.cltvExpiryDelta) } @Test @@ -113,6 +117,10 @@ class OfferManagerTestsCommon : LightningTestSuite() { assertIs(payInvoice) assertEquals(OfferInvoiceReceived(payOffer, payInvoice.invoice), bobOfferManager.eventsFlow.first()) assertEquals(payOffer, payInvoice.payOffer) + assertEquals(1, payInvoice.invoice.blindedPaths.size) + val path = payInvoice.invoice.blindedPaths.first() + assertEquals(EncodedNodeId(aliceTrampolineKey.publicKey()), path.route.route.introductionNodeId) + assertEquals(aliceOfferManager.nodeParams.expiryDeltaBlocks + aliceOfferManager.nodeParams.minFinalCltvExpiryDelta, path.paymentInfo.cltvExpiryDelta) } @Test