Skip to content

Commit 7309540

Browse files
committed
Increase blinded path expiry
Blinded paths should expire when we will start rejecting payments for the corresponding invoice: this is thus related to the bolt 12 invoice expiry that we used. However, when we receive an HTLC, its `cltv_expiry` is set to a future block height, which must be at least `min_final_expiry_delta` in the future. Payers may add some additional margin to the current block height to protect against delays in HTLC relay and protect the privacy of the payment. So we must add a large enough `cltv_expiry_delta` to the invoice expiry to account for those.
1 parent adade12 commit 7309540

File tree

2 files changed

+14
-8
lines changed

2 files changed

+14
-8
lines changed

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ import fr.acinq.bitcoin.ByteVector32
44
import fr.acinq.bitcoin.PublicKey
55
import fr.acinq.bitcoin.utils.Either.Left
66
import fr.acinq.bitcoin.utils.Either.Right
7-
import fr.acinq.lightning.EncodedNodeId
8-
import fr.acinq.lightning.Features
7+
import fr.acinq.lightning.*
98
import fr.acinq.lightning.Lightning.randomBytes32
109
import fr.acinq.lightning.Lightning.randomKey
11-
import fr.acinq.lightning.NodeParams
12-
import fr.acinq.lightning.WalletParams
1310
import fr.acinq.lightning.crypto.RouteBlinding
1411
import fr.acinq.lightning.io.OfferInvoiceReceived
1512
import fr.acinq.lightning.io.OfferNotPaid
@@ -181,8 +178,12 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
181178
maxHtlc = amount * 2,
182179
allowedFeatures = Features.empty
183180
)
184-
// We assume 10 minutes between each block to convert the invoice expiry to a CLTV expiry for the blinded path.
185-
val pathExpiry = (paymentInfo.cltvExpiryDelta + (nodeParams.bolt12InvoiceExpiry.inWholeMinutes.toInt() / 10)).toCltvExpiry(currentBlockHeight.toLong())
181+
// Once the invoice expires, the blinded path shouldn't be usable anymore.
182+
// We assume 10 minutes between each block to convert the invoice expiry to a cltv_expiry_delta.
183+
// When paying the invoice, payers may add any number of blocks to the current block height to protect recipient privacy.
184+
// We assume that they won't add more than 720 blocks, which is reasonable because adding a large delta increases the risk
185+
// that intermediate nodes reject the payment because they don't want their funds potentially locked for a long duration.
186+
val pathExpiry = (paymentInfo.cltvExpiryDelta + CltvExpiryDelta(720) + (nodeParams.bolt12InvoiceExpiry.inWholeMinutes.toInt() / 10)).toCltvExpiry(currentBlockHeight.toLong())
186187
val remoteNodePayload = RouteBlindingEncryptedData(
187188
TlvStream(
188189
RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId.WithPublicKey.Wallet(nodeParams.nodeId)),

src/commonTest/kotlin/fr/acinq/lightning/payment/OfferManagerTestsCommon.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,18 @@ class OfferManagerTestsCommon : LightningTestSuite() {
7171
val offer = createOffer(aliceOfferManager, amount = 1000.msat)
7272

7373
// Bob sends an invoice request to Alice.
74+
val currentBlockHeight = 0
7475
val payOffer = PayOffer(UUID.randomUUID(), randomKey(), null, 5500.msat, offer, 20.seconds)
7576
val (_, invoiceRequests) = bobOfferManager.requestInvoice(payOffer)
7677
assertTrue(invoiceRequests.size == 1)
7778
val (messageForAlice, nextNodeAlice) = trampolineRelay(invoiceRequests.first(), aliceTrampolineKey)
7879
assertEquals(Either.Right(EncodedNodeId.WithPublicKey.Wallet(TestConstants.Alice.nodeParams.nodeId)), nextNodeAlice)
7980
// Alice sends an invoice back to Bob.
80-
val invoiceResponse = aliceOfferManager.receiveMessage(messageForAlice, listOf(), 0)
81+
val invoiceResponse = aliceOfferManager.receiveMessage(messageForAlice, listOf(), currentBlockHeight)
8182
assertIs<OnionMessageAction.SendMessage>(invoiceResponse)
8283
val (messageForBob, nextNodeBob) = trampolineRelay(invoiceResponse.message, aliceTrampolineKey)
8384
assertEquals(Either.Right(EncodedNodeId.WithPublicKey.Wallet(TestConstants.Bob.nodeParams.nodeId)), nextNodeBob)
84-
val payInvoice = bobOfferManager.receiveMessage(messageForBob, listOf(), 0)
85+
val payInvoice = bobOfferManager.receiveMessage(messageForBob, listOf(), currentBlockHeight)
8586
assertIs<OnionMessageAction.PayInvoice>(payInvoice)
8687
assertEquals(OfferInvoiceReceived(payOffer, payInvoice.invoice), bobOfferManager.eventsFlow.first())
8788
assertEquals(payOffer, payInvoice.payOffer)
@@ -91,6 +92,10 @@ class OfferManagerTestsCommon : LightningTestSuite() {
9192
assertEquals(aliceOfferManager.nodeParams.expiryDeltaBlocks + aliceOfferManager.nodeParams.minFinalCltvExpiryDelta, path.paymentInfo.cltvExpiryDelta)
9293
assertEquals(TestConstants.Alice.nodeParams.htlcMinimum, path.paymentInfo.minHtlc)
9394
assertEquals(payOffer.amount * 2, path.paymentInfo.maxHtlc)
95+
// The blinded path expires long after the invoice expiry to allow senders to add their own expiry delta.
96+
val (alicePayload, _) = RouteBlinding.decryptPayload(aliceTrampolineKey, path.route.route.blindingKey, path.route.route.encryptedPayloads.first()).right!!
97+
val paymentConstraints = RouteBlindingEncryptedData.read(alicePayload.toByteArray()).right!!.paymentConstraints!!
98+
assertTrue(paymentConstraints.maxCltvExpiry > CltvExpiryDelta(720).toCltvExpiry(currentBlockHeight.toLong()))
9499
}
95100

96101
@Test

0 commit comments

Comments
 (0)