Skip to content

Commit 856e236

Browse files
Identify failing node by its index (#3224)
When a HTLC is failed remotely, the failing node was previously identified by its node id. However this is not enough if the same node appears multiple times in the payment route, for instance for circular rebalancing. We now identify the failing node using its index in the payment route.
1 parent 0318fb7 commit 856e236

16 files changed

Lines changed: 89 additions & 91 deletions

eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,10 @@ object Sphinx extends Logging {
273273
* A properly decrypted failure from a node in the route.
274274
*
275275
* @param originNode public key of the node that generated the failure.
276+
* @param index position in the route of the node that generated the failure.
276277
* @param failureMessage friendly failure message.
277278
*/
278-
case class DecryptedFailurePacket(originNode: PublicKey, failureMessage: FailureMessage)
279+
case class DecryptedFailurePacket(originNode: PublicKey, index: Int, failureMessage: FailureMessage)
279280

280281
/**
281282
* The downstream failure could not be decrypted.
@@ -335,16 +336,16 @@ object Sphinx extends Logging {
335336
* @return failure message if the origin of the packet could be identified and the packet decrypted, the unwrapped
336337
* failure packet otherwise.
337338
*/
338-
def decrypt(packet: ByteVector, attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret]): HtlcFailure = {
339+
def decrypt(packet: ByteVector, attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret], index: Int = 1): HtlcFailure = {
339340
sharedSecrets match {
340341
case Nil => HtlcFailure(Nil, Left(CannotDecryptFailurePacket(packet, attribution_opt)))
341342
case ss :: tail =>
342343
val packet1 = wrap(packet, ss.secret)
343344
val attribution1_opt = attribution_opt.flatMap(Attribution.unwrap(_, packet1, ss.secret, sharedSecrets.length))
344345
val um = generateKey("um", ss.secret)
345346
val HtlcFailure(downstreamHoldTimes, failure) = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match {
346-
case Attempt.Successful(value) => HtlcFailure(Nil, Right(DecryptedFailurePacket(ss.remoteNodeId, value.value)))
347-
case _ => decrypt(packet1, attribution1_opt.map(_._2), tail)
347+
case Attempt.Successful(value) => HtlcFailure(Nil, Right(DecryptedFailurePacket(ss.remoteNodeId, index, value.value)))
348+
case _ => decrypt(packet1, attribution1_opt.map(_._2), tail, index + 1)
348349
}
349350
HtlcFailure(attribution1_opt.map(n => HoldTime(n._1, ss.remoteNodeId) +: downstreamHoldTimes).getOrElse(Nil), failure)
350351
}

eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ object PaymentFailedSummarySerializer extends ConvertClassSerializer[PaymentFail
375375
val route = f.route.map(_.nodeId) ++ f.route.lastOption.map(_.nextNodeId)
376376
val message = f match {
377377
case LocalFailure(_, _, t) => t.getMessage
378-
case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, failureMessage)) => s"$origin returned: ${failureMessage.message}"
378+
case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, _, failureMessage)) => s"$origin returned: ${failureMessage.message}"
379379
case _: UnreadableRemoteFailure => "unreadable remote failure"
380380
}
381381
PaymentFailureSummaryJson(f.amount, route, message)

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ object PaymentFailure {
194194
*/
195195
def hasAlreadyFailedOnce(nodeId: PublicKey, failures: Seq[PaymentFailure]): Boolean =
196196
failures
197-
.collectFirst { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, u: Update)) if origin == nodeId => u.update_opt }
197+
.collectFirst { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, _, u: Update)) if origin == nodeId => u.update_opt }
198198
.isDefined
199199

200200
/** Ignore the channel outgoing from the given nodeId in the given route. */
@@ -213,12 +213,12 @@ object PaymentFailure {
213213

214214
/** Update the set of nodes and channels to ignore in retries depending on the failure we received. */
215215
def updateIgnored(failure: PaymentFailure, ignore: Ignore): Ignore = failure match {
216-
case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _)) if nodeId == hops.last.nextNodeId =>
216+
case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, _)) if nodeId == hops.last.nextNodeId =>
217217
// The failure came from the final recipient: the payment should be aborted without penalizing anyone in the route.
218218
ignore
219-
case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(nodeId, _: Node)) =>
219+
case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(nodeId, _, _: Node)) =>
220220
ignore + nodeId
221-
case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
221+
case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage: Update)) =>
222222
if (failureMessage.update_opt.forall(update => Announcements.checkSig(update, nodeId))) {
223223
val shouldIgnore = failureMessage match {
224224
case _: TemporaryChannelFailure => true
@@ -235,7 +235,7 @@ object PaymentFailure {
235235
// This node is fishy, it gave us a bad channel update signature, so let's filter it out.
236236
ignore + nodeId
237237
}
238-
case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _)) =>
238+
case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, _)) =>
239239
ignoreNodeOutgoingEdge(nodeId, hops, ignore)
240240
case UnreadableRemoteFailure(_, hops, _, holdTimes) =>
241241
// TODO: Once everyone supports attributable errors, we should only exclude two nodes: the last for which we have attribution data and the next one.
@@ -267,7 +267,7 @@ object PaymentFailure {
267267
// We're only interested in the last channel update received per channel.
268268
val updates = failures.foldLeft(Map.empty[ShortChannelId, ChannelUpdate]) {
269269
case (current, failure) => failure match {
270-
case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f: Update)) => f.update_opt match {
270+
case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, _, f: Update)) => f.update_opt match {
271271
case Some(update) => current.updated(update.shortChannelId, update)
272272
case None => current
273273
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto
7676

7777
case paymentResult: PaymentEvent =>
7878
paymentResult match {
79-
case PaymentFailed(_, _, _ :+ RemoteFailure(_, _, DecryptedFailurePacket(targetNodeId, _: IncorrectOrUnknownPaymentDetails)), _) =>
79+
case PaymentFailed(_, _, _ :+ RemoteFailure(_, _, DecryptedFailurePacket(targetNodeId, _, _: IncorrectOrUnknownPaymentDetails)), _) =>
8080
log.info(s"payment probe successful to node=$targetNodeId")
8181
case _ =>
8282
log.info(s"payment probe failed with paymentResult=$paymentResult")

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

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -208,43 +208,40 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
208208
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(request.amount, Nil, e, htlcFailure.holdTimes))).increment()
209209
failure
210210
}) match {
211-
case res@Right(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
211+
case res@Right(Sphinx.DecryptedFailurePacket(_, index, failureMessage)) =>
212212
// We have discovered some liquidity information with this payment: we update the router accordingly.
213-
val stoppedRoute = route.stopAt(nodeId)
213+
val stoppedRoute = route.stopAt(index)
214214
if (stoppedRoute.hops.length > 1) {
215215
router ! Router.RouteCouldRelay(stoppedRoute)
216216
}
217217
failureMessage match {
218218
case TemporaryChannelFailure(update_opt, _) =>
219-
route.hops.find(_.nodeId == nodeId) match {
220-
case Some(failingHop) =>
221-
val isLiquidityIssue = update_opt match {
222-
// If the relay parameters have changed, it's not necessarily a liquidity issue.
223-
case Some(update) => HopRelayParams.areSame(failingHop.params, HopRelayParams.FromAnnouncement(update), ignoreHtlcSize = true)
224-
case None => true
225-
}
226-
if (isLiquidityIssue) {
227-
router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop)
228-
}
229-
case _ => ()
219+
val failingHop = route.hops(index)
220+
val isLiquidityIssue = update_opt match {
221+
// If the relay parameters have changed, it's not necessarily a liquidity issue.
222+
case Some(update) => HopRelayParams.areSame(failingHop.params, HopRelayParams.FromAnnouncement(update), ignoreHtlcSize = true)
223+
case None => true
224+
}
225+
if (isLiquidityIssue) {
226+
router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop)
230227
}
231228
case _ => // other errors should not be used for liquidity issues
232229
}
233230
res
234231
case res => res
235232
}) match {
236-
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == recipient.nodeId =>
233+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) if nodeId == recipient.nodeId =>
237234
// if destination node returns an error, we fail the payment immediately
238235
log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)")
239236
myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(request.amount, route.fullRoute, e))))
240-
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if route.finalHop_opt.collect { case h: NodeHop if h.nodeId == nodeId => h }.nonEmpty =>
237+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) if route.finalHop_opt.collect { case h: NodeHop if h.nodeId == nodeId => h }.nonEmpty =>
241238
// if trampoline node returns an error, we fail the payment immediately
242239
log.warning(s"received an error message from trampoline nodeId=$nodeId, failing the payment (failure=$failureMessage)")
243240
myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(request.amount, route.fullRoute, e))))
244241
case res if failures.size + 1 >= request.maxAttempts =>
245242
// otherwise we never try more than maxAttempts, no matter the kind of error returned
246243
val failure = res match {
247-
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
244+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) =>
248245
log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)")
249246
failureMessage match {
250247
case failureMessage: Update => handleUpdate(nodeId, failureMessage, d)
@@ -261,11 +258,11 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
261258
log.warning(s"cannot parse returned error: unwrapped=$unwrapped, route=${route.printNodes()}")
262259
val failure = UnreadableRemoteFailure(request.amount, route.fullRoute, e, htlcFailure.holdTimes)
263260
retry(failure, d)
264-
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) =>
261+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage: Node)) =>
265262
log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)")
266263
val failure = RemoteFailure(request.amount, route.fullRoute, e)
267264
retry(failure, d)
268-
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
265+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage: Update)) =>
269266
log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)")
270267
val failure = RemoteFailure(request.amount, route.fullRoute, e)
271268
if (failureMessage.update_opt.forall(update => Announcements.checkSig(update, nodeId))) {
@@ -292,7 +289,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
292289
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore + nodeId)
293290
}
294291
}
295-
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _: InvalidOnionBlinding)) =>
292+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, _: InvalidOnionBlinding)) =>
296293
// there was a failure inside the blinded route we used: we cannot know why it failed, so let's ignore it.
297294
log.info(s"received an error coming from nodeId=$nodeId inside the blinded route, retrying with different blinded routes")
298295
val failure = RemoteFailure(request.amount, route.fullRoute, e)
@@ -305,7 +302,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
305302
router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
306303
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore1)
307304
}
308-
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
305+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) =>
309306
log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)")
310307
val failure = RemoteFailure(request.amount, route.fullRoute, e)
311308
retry(failure, d)

eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -692,9 +692,9 @@ object Router {
692692

693693
def printChannels(): String = hops.map(_.shortChannelId).mkString("->")
694694

695-
def stopAt(nodeId: PublicKey): Route = {
696-
val amountAtStop = hops.reverse.takeWhile(_.nextNodeId != nodeId).foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) }
697-
Route(amountAtStop, hops.takeWhile(_.nodeId != nodeId), None)
695+
def stopAt(index: Int): Route = {
696+
val amountAtStop = hops.drop(index).foldRight(amount) { case (hop, amount1) => amount1 + hop.fee(amount1) }
697+
Route(amountAtStop, hops.take(index), None)
698698
}
699699
}
700700

0 commit comments

Comments
 (0)