From c22094fc2dadb5052b566ee4a1599eb15612e934 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Mon, 19 Jul 2021 18:55:50 +0200 Subject: [PATCH] Fix pay-to-open below minimum when using MPP (#1892) Compare the *sum* of all pay-to-open parts against the min pay-to-open amount. --- .../payment/receive/MultiPartPaymentFSM.scala | 7 ++- .../payment/MultiPartPaymentFSMSpec.scala | 55 ++++++++++++++++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala index cfc461bcf..226926c26 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala @@ -70,9 +70,10 @@ class MultiPartPaymentFSM(nodeParams: NodeParams, paymentHash: ByteVector32, tot // That is why, instead, we wait for all parts to arrive. Then, if there is at least one pay-to-open part, and if // the total received amount is less than the minimum amount required for a pay-to-open, we fail the payment. updatedParts - .collectFirst { case p: PayToOpenPart => p } match { - case Some(p) if p.totalAmount < p.payToOpen.payToOpenMinAmount => - context.system.eventStream.publish(MissedPayToOpenPayment(p.paymentHash, p.totalAmount, p.payToOpen.payToOpenMinAmount)) + .collect { case p: PayToOpenPart => p.payToOpen } match { + case payToOpenRequests if payToOpenRequests.nonEmpty && PayToOpenRequest.combine(payToOpenRequests).amountMsat < payToOpenRequests.head.payToOpenMinAmount => + val p = payToOpenRequests.head + context.system.eventStream.publish(MissedPayToOpenPayment(p.paymentHash, part.totalAmount, p.payToOpenMinAmount)) goto(PAYMENT_FAILED) using PaymentFailed(IncorrectOrUnknownPaymentDetails(part.totalAmount, nodeParams.currentBlockHeight), updatedParts) case _ => goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(updatedParts) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala index deac5a7ff..29c752ff2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala @@ -16,13 +16,14 @@ package fr.acinq.eclair.payment +import akka.actor.ActorRef import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.testkit.{TestActorRef, TestProbe} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM._ -import fr.acinq.eclair.wire.{IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} -import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, MilliSatoshi, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, wire} +import fr.acinq.eclair.wire.{IncorrectOrUnknownPaymentDetails, PayToOpenRequest, UpdateAddHtlc} +import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, MilliSatoshi, NodeParams, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, randomBytes32, wire} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector @@ -110,6 +111,21 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.eventListener.expectNoMsg(50 millis) } + test("fail all if total pay-to-open is below minimum") { + val f = createFixture(250 millis, 20000000 msat) + f.parent.send(f.handler, createMultiPartHtlc(20000000 msat, 16000000 msat, 1)) + val payToOpenAmount = 4000000.msat + assert(payToOpenMinAmount > payToOpenAmount) + f.parent.send(f.handler, createPayToOpenPart(20000000 msat, payToOpenAmount)) + val fail = f.parent.expectMsgType[MultiPartPaymentFailed] + assert(fail.paymentHash === paymentHash) + assert(fail.failure === IncorrectOrUnknownPaymentDetails(20000000 msat, f.currentBlockHeight)) + assert(fail.parts.length === 2) + + f.parent.expectNoMsg(50 millis) + f.eventListener.expectNoMsg(50 millis) + } + test("fulfill all when total amount reached") { val f = createFixture(10 seconds, 1000 msat) val parts = Seq( @@ -167,6 +183,22 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.eventListener.expectNoMsg(50 millis) } + test("fulfill all if total pay-to-open is above minimum") { + val f = createFixture(250 millis, 20000000 msat) + val parts = Seq( + createMultiPartHtlc(20000000 msat, 6000000 msat, 1), + createPayToOpenPart(20000000 msat, 7000000 msat), + createPayToOpenPart(20000000 msat, 7000000 msat) + ) + parts.foreach(p => f.parent.send(f.handler, p)) + + val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] + assert(paymentResult.parts.toSet === parts.toSet) + + f.parent.expectNoMsg(50 millis) + f.eventListener.expectNoMsg(50 millis) + } + test("receive additional htlcs after total amount reached") { val f = createFixture(10 seconds, 1000 msat) @@ -235,4 +267,21 @@ object MultiPartPaymentFSMSpec { HtlcPart(totalAmount, htlc) } + val payToOpenMinAmount = 10000.sat.toMilliSatoshi + + def createPayToOpenPart(totalAmount: MilliSatoshi, payToOpenAmount: MilliSatoshi): PayToOpenPart = + PayToOpenPart( + totalAmount = totalAmount, + payToOpen = PayToOpenRequest( + chainHash = Block.RegtestGenesisBlock.blockId, + fundingSatoshis = payToOpenAmount.truncateToSatoshi * 2, + amountMsat = payToOpenAmount, + payToOpenFee = 10 sat, + paymentHash = paymentHash, + expireAt = Long.MaxValue, + htlc_opt = None, + payToOpenMinAmount = payToOpenMinAmount), + peer = ActorRef.noSender + ) + } \ No newline at end of file