Skip to content

Commit

Permalink
Fix payments using FromChannelBalanceForFutureHtlc
Browse files Browse the repository at this point in the history
We forgot to match the `payment_hash` for this payment type, and also
didn't check that the `funding_fee` was `0 msat`.
  • Loading branch information
t-bast committed Sep 18, 2024
1 parent 4031190 commit 0877f36
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IncomingPaymentHandler.ProcessAddResult.Pending>(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<IncomingPaymentHandler.ProcessAddResult.Accepted>(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()
Expand Down

0 comments on commit 0877f36

Please sign in to comment.