diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt index cddff9717..55d70ac4f 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt @@ -339,15 +339,15 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb, pri when (val purchase = db.getInboundLiquidityPurchase(fundingTxId)?.purchase) { null -> Either.Left(UnexpectedLiquidityAdsFundingFee(channelId, fundingTxId)) else -> { - val paymentHashOk = when (val details = purchase.paymentDetails) { - is LiquidityAds.PaymentDetails.FromFutureHtlc -> details.paymentHashes.contains(paymentHash) - is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> details.preimages.any { Crypto.sha256(it).byteVector32() == paymentHash } + val fundingFeeOk = when (val details = purchase.paymentDetails) { + is LiquidityAds.PaymentDetails.FromFutureHtlc -> details.paymentHashes.contains(paymentHash) && fundingFee <= purchase.fees.total.toMilliSatoshi() + is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> details.preimages.any { Crypto.sha256(it).byteVector32() == paymentHash } && fundingFee <= purchase.fees.total.toMilliSatoshi() + // Fees have already been paid from our channel balance. + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> details.paymentHashes.contains(paymentHash) && fundingFee == 0.msat is LiquidityAds.PaymentDetails.FromChannelBalance -> false - is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> false } - val feeAmountOk = fundingFee <= purchase.fees.total.toMilliSatoshi() when { - paymentHashOk && feeAmountOk -> Either.Right(LiquidityAds.FundingFee(fundingFee, fundingTxId)) + fundingFeeOk -> Either.Right(LiquidityAds.FundingFee(fundingFee, fundingTxId)) else -> Either.Left(InvalidLiquidityAdsFundingFee(channelId, fundingTxId, paymentHash, purchase.fees.total, fundingFee)) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt index 21371176d..cd2a7afc5 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt @@ -259,6 +259,7 @@ object LiquidityAds { // The user should retry this funding attempt without requesting inbound liquidity. null -> Either.Left(MissingLiquidityAds(channelId)) else -> when { + // Note that we use fundingRate instead of willFund.fundingRate: this way we verify that the funding rates match. !Crypto.verifySignature(fundingRate.signedData(fundingScript), willFund.signature, remoteNodeId) -> Either.Left(InvalidLiquidityAdsSig(channelId)) remoteFundingAmount < requestedAmount -> Either.Left(InvalidLiquidityAdsAmount(channelId, remoteFundingAmount, requestedAmount)) willFund.fundingRate != fundingRate -> Either.Left(InvalidLiquidityAdsRate(channelId)) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt index 88a5be758..dedfb587e 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt @@ -673,6 +673,54 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { } } + @Test + fun `receive payment with funding fee -- from channel balance`() = runSuspendTest { + val channelId = randomBytes32() + val amount = 50_000_000.msat + val (paymentHandler, incomingPayment, paymentSecret) = createFixture(amount) + checkDbPayment(incomingPayment, paymentHandler.db) + + // Step 1 of 2: + // - Alice sends will_add_htlc to Bob + // - Bob triggers an open/splice + val purchase = run { + val willAddHtlc = makeWillAddHtlc(paymentHandler, incomingPayment.paymentHash, makeMppPayload(amount, amount, paymentSecret)) + val result = paymentHandler.process(willAddHtlc, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw) + assertIs(result) + assertEquals(1, result.actions.size) + val splice = result.actions.first() as AddLiquidityForIncomingPayment + // The splice transaction is successfully signed and stored in the DB. + val purchase = LiquidityAds.Purchase.Standard( + splice.requestedAmount, + splice.fees(TestConstants.feeratePerKw, isChannelCreation = false), + LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(listOf(incomingPayment.paymentHash)), + ) + val payment = InboundLiquidityOutgoingPayment(UUID.randomUUID(), channelId, TxId(randomBytes32()), 500.sat, purchase, 0, null, null) + paymentHandler.db.addOutgoingPayment(payment) + payment + } + + // Step 2 of 2: + // - After the splice completes, Alice sends a second HTLC to Bob without deducting the funding fee (it was paid from the channel balance) + // - Bob accepts the MPP set + run { + val fundingFee = purchase.fundingFee.copy(amount = 0.msat) + val htlc = makeUpdateAddHtlc(7, channelId, paymentHandler, incomingPayment.paymentHash, makeMppPayload(amount, amount, paymentSecret), fundingFee = fundingFee) + assertEquals(htlc.amountMsat, amount) + val result = paymentHandler.process(htlc, TestConstants.defaultBlockHeight, TestConstants.feeratePerKw) + assertIs(result) + val (expectedActions, expectedReceivedWith) = setOf( + // @formatter:off + WrappedChannelCommand(channelId, ChannelCommand.Htlc.Settlement.Fulfill(7, incomingPayment.preimage, commit = true)) to IncomingPayment.ReceivedWith.LightningPayment(amount, channelId, 7, fundingFee), + // @formatter:on + ).unzip() + assertEquals(expectedActions.toSet(), result.actions.toSet()) + assertEquals(amount, result.received.amount) + assertEquals(expectedReceivedWith, result.received.receivedWith) + checkDbPayment(result.incomingPayment, paymentHandler.db) + } + } + @Test fun `receive payment with funding fee -- unknown transaction`() = runSuspendTest { val channelId = randomBytes32()