Skip to content

Commit 0805d51

Browse files
Do not retry sending if payment gets confirmed on chain (#1799)
The `PaymentLifecycle` state machine already had that mechanism, but the `MultiPartPaymentLifecycle` didn't.
1 parent 898c17b commit 0805d51

File tree

3 files changed

+34
-15
lines changed

3 files changed

+34
-15
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala

+14-9
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616

1717
package fr.acinq.eclair.payment.send
1818

19+
import java.util.UUID
20+
import java.util.concurrent.TimeUnit
21+
1922
import akka.actor.{ActorRef, FSM, Props, Status}
2023
import akka.event.Logging.MDC
2124
import fr.acinq.bitcoin.ByteVector32
2225
import fr.acinq.bitcoin.Crypto.PublicKey
26+
import fr.acinq.eclair.channel.{HtlcOverriddenByLocalCommit, HtlcsTimedoutDownstream, HtlcsWillTimeoutUpstream}
2327
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType}
2428
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
2529
import fr.acinq.eclair.payment.OutgoingPacket.Upstream
@@ -33,9 +37,6 @@ import fr.acinq.eclair.router.Router._
3337
import fr.acinq.eclair.wire.protocol._
3438
import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams}
3539

36-
import java.util.UUID
37-
import java.util.concurrent.TimeUnit
38-
3940
/**
4041
* Created by t-bast on 18/07/2019.
4142
*/
@@ -112,7 +113,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
112113
}
113114

114115
case Event(pf: PaymentFailed, d: PaymentProgress) =>
115-
if (isFinalRecipientFailure(pf, d)) {
116+
if (abortPayment(pf, d)) {
116117
gotoAbortedOrStop(PaymentAborted(d.request, d.failures ++ pf.failures, d.pending.keySet - pf.id))
117118
} else {
118119
val ignore1 = PaymentFailure.updateIgnored(pf.failures, d.ignore)
@@ -130,7 +131,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
130131

131132
when(PAYMENT_IN_PROGRESS) {
132133
case Event(pf: PaymentFailed, d: PaymentProgress) =>
133-
if (isFinalRecipientFailure(pf, d)) {
134+
if (abortPayment(pf, d)) {
134135
gotoAbortedOrStop(PaymentAborted(d.request, d.failures ++ pf.failures, d.pending.keySet - pf.id))
135136
} else if (d.remainingAttempts == 0) {
136137
val failure = LocalFailure(Nil, PaymentError.RetryExhausted)
@@ -377,10 +378,14 @@ object MultiPartPaymentLifecycle {
377378
SendPaymentToRoute(replyTo, Right(route), finalPayload)
378379
}
379380

380-
/** When we receive an error from the final recipient, we should fail the whole payment, it's useless to retry. */
381-
private def isFinalRecipientFailure(pf: PaymentFailed, d: PaymentProgress): Boolean = pf.failures.collectFirst {
382-
case f: RemoteFailure if f.e.originNode == d.request.targetNodeId => true
383-
}.getOrElse(false)
381+
/** When we receive an error from the final recipient or payment gets settled on chain, we should fail the whole payment, it's useless to retry. */
382+
private def abortPayment(pf: PaymentFailed, d: PaymentProgress): Boolean = pf.failures.exists {
383+
case f: RemoteFailure => f.e.originNode == d.request.targetNodeId
384+
case LocalFailure(_, _: HtlcOverriddenByLocalCommit) => true
385+
case LocalFailure(_, _: HtlcsWillTimeoutUpstream) => true
386+
case LocalFailure(_, _: HtlcsTimedoutDownstream) => true
387+
case _ => false
388+
}
384389

385390
private def remainingToSend(nodeParams: NodeParams, request: SendMultiPartPayment, pending: Iterable[Route]): (MilliSatoshi, MilliSatoshi) = {
386391
val sentAmount = pending.map(_.amount).sum

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,12 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
141141
}
142142
// we only retry if the error isn't fatal, and we haven't exhausted the max number of retried
143143
val doRetry = !isFatal && (d.failures.size + 1 < d.c.maxAttempts)
144+
val localFailure = LocalFailure(cfg.fullRoute(d.route), t)
144145
if (doRetry) {
145146
log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})")
146-
val failure = LocalFailure(cfg.fullRoute(d.route), t)
147-
retry(failure, d)
147+
retry(localFailure, d)
148148
} else {
149-
onFailure(d.c.replyTo, PaymentFailed(id, paymentHash, d.failures :+ LocalFailure(cfg.fullRoute(d.route), t)))
149+
onFailure(d.c.replyTo, PaymentFailed(id, paymentHash, d.failures :+ localFailure))
150150
stop(FSM.Normal)
151151
}
152152
}

eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala

+17-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ package fr.acinq.eclair.payment
1818

1919
import akka.actor.{ActorContext, ActorRef, Status}
2020
import akka.testkit.{TestFSMRef, TestProbe}
21-
import fr.acinq.bitcoin.{Block, Crypto}
21+
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto}
2222
import fr.acinq.eclair._
23-
import fr.acinq.eclair.channel.{ChannelFlags, ChannelUnavailable}
23+
import fr.acinq.eclair.channel.{ChannelFlags, ChannelUnavailable, HtlcsTimedoutDownstream}
2424
import fr.acinq.eclair.crypto.Sphinx
2525
import fr.acinq.eclair.db.{FailureSummary, FailureType, OutgoingPaymentStatus}
2626
import fr.acinq.eclair.payment.OutgoingPacket.Upstream
@@ -36,8 +36,8 @@ import fr.acinq.eclair.wire.protocol._
3636
import org.scalatest.Outcome
3737
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
3838
import scodec.bits.{ByteVector, HexStringSyntax}
39-
4039
import java.util.UUID
40+
4141
import scala.concurrent.duration._
4242

4343
/**
@@ -384,6 +384,20 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
384384
assert(result.failures.length === 1)
385385
}
386386

387+
test("abort if payment gets settled on chain") { f =>
388+
import f._
389+
390+
val payment = SendMultiPartPayment(sender.ref, randomBytes32, e, finalAmount, expiry, 5, routeParams = Some(routeParams))
391+
sender.send(payFsm, payment)
392+
router.expectMsgType[RouteRequest]
393+
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
394+
childPayFsm.expectMsgType[SendPaymentToRoute]
395+
396+
val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
397+
val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.hops, HtlcsTimedoutDownstream(channelId = ByteVector32.One, htlcs = Set.empty)))))
398+
assert(result.failures.length === 1)
399+
}
400+
387401
test("abort if recipient sends error during retry") { f =>
388402
import f._
389403

0 commit comments

Comments
 (0)