Skip to content

Commit

Permalink
Include min_final_cltv_expiry_delta in blinded paths
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast committed Aug 21, 2024
1 parent fb0ae19 commit e1b162a
Show file tree
Hide file tree
Showing 2 changed files with 15 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,23 @@ 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
)
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ class OfferManagerTestsCommon : LightningTestSuite() {
assertIs<OnionMessageAction.PayInvoice>(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
Expand Down Expand Up @@ -113,6 +117,10 @@ class OfferManagerTestsCommon : LightningTestSuite() {
assertIs<OnionMessageAction.PayInvoice>(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
Expand Down

0 comments on commit e1b162a

Please sign in to comment.