Skip to content

Commit 2772138

Browse files
authored
Better handling of TemporaryChannelFailure (#1726)
When a node returns a TemporaryChannelFailure, we should ignore this channel when retrying. In some cases (such as channels from routing hints) this was not correctly handled. Fixes #1725
1 parent 6364ae3 commit 2772138

File tree

4 files changed

+57
-17
lines changed

4 files changed

+57
-17
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala

+24-11
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import fr.acinq.eclair.crypto.Sphinx
2222
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
2323
import fr.acinq.eclair.router.Announcements
2424
import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore}
25-
import fr.acinq.eclair.wire.{ChannelUpdate, Node}
25+
import fr.acinq.eclair.wire.{ChannelDisabled, ChannelUpdate, Node, TemporaryChannelFailure}
2626
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId}
2727

2828
import java.util.UUID
@@ -158,29 +158,42 @@ object PaymentFailure {
158158
.collectFirst { case RemoteFailure(_, Sphinx.DecryptedFailurePacket(origin, u: Update)) if origin == nodeId => u.update }
159159
.isDefined
160160

161+
/** Ignore the channel outgoing from the given nodeId in the given route. */
162+
private def ignoreNodeOutgoingChannel(nodeId: PublicKey, hops: Seq[Hop], ignore: Ignore): Ignore = {
163+
hops.collectFirst {
164+
case hop: ChannelHop if hop.nodeId == nodeId => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId)
165+
} match {
166+
case Some(faultyChannel) => ignore + faultyChannel
167+
case None => ignore
168+
}
169+
}
170+
161171
/** Update the set of nodes and channels to ignore in retries depending on the failure we received. */
162172
def updateIgnored(failure: PaymentFailure, ignore: Ignore): Ignore = failure match {
163173
case RemoteFailure(hops, Sphinx.DecryptedFailurePacket(nodeId, _)) if nodeId == hops.last.nextNodeId =>
164174
// The failure came from the final recipient: the payment should be aborted without penalizing anyone in the route.
165175
ignore
166176
case RemoteFailure(_, Sphinx.DecryptedFailurePacket(nodeId, _: Node)) =>
167177
ignore + nodeId
168-
case RemoteFailure(_, Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
178+
case RemoteFailure(hops, Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
169179
if (Announcements.checkSig(failureMessage.update, nodeId)) {
170-
// We were using an outdated channel update, we should retry with the new one and nobody should be penalized.
171-
ignore
180+
val shouldIgnore = failureMessage match {
181+
case _: TemporaryChannelFailure => true
182+
case _: ChannelDisabled => true
183+
case _ => false
184+
}
185+
if (shouldIgnore) {
186+
ignoreNodeOutgoingChannel(nodeId, hops, ignore)
187+
} else {
188+
// We were using an outdated channel update, we should retry with the new one and nobody should be penalized.
189+
ignore
190+
}
172191
} else {
173192
// This node is fishy, it gave us a bad signature, so let's filter it out.
174193
ignore + nodeId
175194
}
176195
case RemoteFailure(hops, Sphinx.DecryptedFailurePacket(nodeId, _)) =>
177-
// Let's ignore the channel outgoing from nodeId.
178-
hops.collectFirst {
179-
case hop: ChannelHop if hop.nodeId == nodeId => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId)
180-
} match {
181-
case Some(faultyChannel) => ignore + faultyChannel
182-
case None => ignore
183-
}
196+
ignoreNodeOutgoingChannel(nodeId, hops, ignore)
184197
case UnreadableRemoteFailure(hops) =>
185198
// We don't know which node is sending garbage, let's blacklist all nodes except the one we are directly connected to and the final recipient.
186199
val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1).toSet

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -193,18 +193,20 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
193193
retry(failure, d)
194194
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
195195
log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)")
196+
val failure = RemoteFailure(cfg.fullRoute(route), e)
196197
val ignore1 = if (Announcements.checkSig(failureMessage.update, nodeId)) {
197198
val assistedRoutes1 = handleUpdate(nodeId, failureMessage, d)
199+
val ignore1 = PaymentFailure.updateIgnored(failure, ignore)
198200
// let's try again, router will have updated its state
199-
router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), assistedRoutes1, ignore, c.routeParams, paymentContext = Some(cfg.paymentContext))
200-
ignore
201+
router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), assistedRoutes1, ignore1, c.routeParams, paymentContext = Some(cfg.paymentContext))
202+
ignore1
201203
} else {
202204
// this node is fishy, it gave us a bad sig!! let's filter it out
203205
log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}")
204206
router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), c.assistedRoutes, ignore + nodeId, c.routeParams, paymentContext = Some(cfg.paymentContext))
205207
ignore + nodeId
206208
}
207-
goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ RemoteFailure(cfg.fullRoute(route), e), ignore1)
209+
goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ failure, ignore1)
208210
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
209211
log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)")
210212
val failure = RemoteFailure(cfg.fullRoute(route), e)

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

+27-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle._
3030
import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted
3131
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
3232
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute
33-
import fr.acinq.eclair.router.RouteNotFound
3433
import fr.acinq.eclair.router.Router._
34+
import fr.acinq.eclair.router.{Announcements, RouteNotFound}
3535
import fr.acinq.eclair.wire._
3636
import org.scalatest.Outcome
3737
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
@@ -261,6 +261,30 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
261261
assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === ExtraHop(b, channelUpdate.shortChannelId, 250 msat, 150, CltvExpiryDelta(24)))
262262
}
263263

264+
test("retry with ignored routing hints (temporary channel failure)") { f =>
265+
import f._
266+
267+
// The B -> E channel is private and provided in the invoice routing hints.
268+
val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta)
269+
val payment = SendMultiPartPayment(sender.ref, randomBytes32, e, finalAmount, expiry, 3, routeParams = Some(routeParams), assistedRoutes = List(List(routingHint)))
270+
sender.send(payFsm, payment)
271+
assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint)
272+
val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
273+
router.send(payFsm, RouteResponse(Seq(route)))
274+
childPayFsm.expectMsgType[SendPaymentToRoute]
275+
childPayFsm.expectNoMsg(100 millis)
276+
277+
// B doesn't have enough liquidity on this channel.
278+
// NB: we need a channel update with a valid signature, otherwise we'll ignore the node instead of this specific channel.
279+
val channelUpdate = Announcements.makeChannelUpdate(hop_be.lastUpdate.chainHash, priv_b, e, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.cltvExpiryDelta, hop_be.lastUpdate.htlcMinimumMsat, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.htlcMaximumMsat.get)
280+
val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head
281+
childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.hops, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdate))))))
282+
// We update the routing hints accordingly before requesting a new route and ignore the channel.
283+
val routeRequest = router.expectMsgType[RouteRequest]
284+
assert(routeRequest.assistedRoutes.head.head === routingHint)
285+
assert(routeRequest.ignore.channels.map(_.shortChannelId) === Set(channelUpdate.shortChannelId))
286+
}
287+
264288
test("update routing hints") { _ =>
265289
val routingHints = Seq(
266290
Seq(ExtraHop(a, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), ExtraHop(b, ShortChannelId(2), 0 msat, 100, CltvExpiryDelta(24))),
@@ -536,7 +560,8 @@ object MultiPartPaymentLifecycleSpec {
536560
* where a has multiple channels with each of his peers.
537561
*/
538562

539-
val a :: b :: c :: d :: e :: Nil = Seq.fill(5)(randomKey.publicKey)
563+
val priv_a :: priv_b :: priv_c :: priv_d :: priv_e :: Nil = Seq.fill(5)(randomKey)
564+
val a :: b :: c :: d :: e :: Nil = Seq(priv_a, priv_b, priv_c, priv_d, priv_e).map(_.publicKey)
540565
val channelId_ab_1 = ShortChannelId(1)
541566
val channelId_ab_2 = ShortChannelId(2)
542567
val channelId_be = ShortChannelId(3)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
392392
// payment lifecycle forwards the embedded channelUpdate to the router
393393
routerForwarder.expectMsg(update_bc)
394394
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
395-
routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg))
395+
routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(update_bc.shortChannelId, b, c)))))
396396
routerForwarder.forward(routerFixture.router)
397397
// we allow 2 tries, so we send a 2nd request to the router
398398
assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(Nil, RouteNotFound) :: Nil)

0 commit comments

Comments
 (0)