Skip to content

Commit 3ffbf6d

Browse files
committed
Assign blame with fat errors
1 parent e7da96b commit 3ffbf6d

File tree

4 files changed

+36
-28
lines changed

4 files changed

+36
-28
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ object FailureSummary {
248248
def apply(f: PaymentFailure): FailureSummary = f match {
249249
case LocalFailure(_, route, t) => FailureSummary(FailureType.LOCAL, t.getMessage, route.map(h => HopSummary(h)).toList, route.headOption.map(_.nodeId))
250250
case RemoteFailure(_, route, e) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.map(h => HopSummary(h)).toList, Some(e.originNode))
251-
case UnreadableRemoteFailure(_, route) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList, None)
251+
case UnreadableRemoteFailure(_, route, e) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList, Some(e.failingNode)) // Could also be the last hop with a payload
252252
}
253253
}
254254

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ object Monitoring {
125125
def apply(pf: PaymentFailure): String = pf match {
126126
case LocalFailure(_, _, t) => t.getClass.getSimpleName
127127
case RemoteFailure(_, _, e) => e.failureMessage.getClass.getSimpleName
128-
case UnreadableRemoteFailure(_, _) => "UnreadableRemoteFailure"
128+
case UnreadableRemoteFailure(_, _, _) => "UnreadableRemoteFailure"
129129
}
130130
}
131131

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ case class LocalFailure(amount: MilliSatoshi, route: Seq[Hop], t: Throwable) ext
136136
case class RemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure
137137

138138
/** A remote node failed the payment but we couldn't decrypt the failure (e.g. a malicious node tampered with the message). */
139-
case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop]) extends PaymentFailure
139+
case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.InvalidFatErrorPacket) extends PaymentFailure
140140

141141
object PaymentFailure {
142142

@@ -217,9 +217,12 @@ object PaymentFailure {
217217
}
218218
case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _)) =>
219219
ignoreNodeOutgoingChannel(nodeId, hops, ignore)
220-
case UnreadableRemoteFailure(_, hops) =>
221-
// 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.
222-
val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1).toSet
220+
case UnreadableRemoteFailure(_, hops, e) =>
221+
// We don't know which node is sending garbage, let's blacklist both nodes, but not the one we are directly connected to or the final recipient.
222+
val blacklist = Set(
223+
if (e.hopPayloads.length > 1) Some(e.hopPayloads.last._1) else None,
224+
if (e.hopPayloads.nonEmpty && e.failingNode != hops.last.nextNodeId) Some(e.failingNode) else None,
225+
).flatten
223226
ignore ++ blacklist
224227
case LocalFailure(_, hops, _) => hops.headOption match {
225228
case Some(hop: ChannelHop) =>

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

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -168,15 +168,15 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
168168

169169
private def handleRemoteFail(d: WaitingForComplete, fail: UpdateFailHtlc) = {
170170
import d._
171-
((Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match {
172-
case success@Success(e) =>
171+
val result = Sphinx.FatErrorPacket.decrypt(fail.reason, sharedSecrets)
172+
result match {
173+
case Right(e) =>
173174
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(d.c.finalPayload.amount, Nil, e))).increment()
174-
success
175-
case failure@Failure(_) =>
176-
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(d.c.finalPayload.amount, Nil))).increment()
177-
failure
178-
}) match {
179-
case res@Success(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
175+
case Left(e) =>
176+
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(d.c.finalPayload.amount, Nil, e))).increment()
177+
}
178+
result match {
179+
case Right(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
180180
// We have discovered some liquidity information with this payment: we update the router accordingly.
181181
val stoppedRoute = d.route.stopAt(nodeId)
182182
if (stoppedRoute.hops.length > 1) {
@@ -191,38 +191,43 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
191191
}
192192
case _ => // other errors should not be used for liquidity issues
193193
}
194-
res
195-
case res => res
196-
}) match {
197-
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId =>
194+
case Left(Sphinx.InvalidFatErrorPacket(reachedNodes, _)) =>
195+
if (reachedNodes.length > 1) {
196+
val lastReachedNode = reachedNodes.last._1
197+
val stoppedRoute = d.route.stopAt(lastReachedNode)
198+
router ! Router.RouteCouldRelay(stoppedRoute)
199+
}
200+
}
201+
result match {
202+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId =>
198203
// if destination node returns an error, we fail the payment immediately
199204
log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)")
200205
myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e))))
201206
case res if failures.size + 1 >= c.maxAttempts =>
202207
// otherwise we never try more than maxAttempts, no matter the kind of error returned
203208
val failure = res match {
204-
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
209+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
205210
log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)")
206211
failureMessage match {
207212
case failureMessage: Update => handleUpdate(nodeId, failureMessage, d)
208213
case _ =>
209214
}
210215
RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e)
211-
case Failure(t) =>
212-
log.warning(s"cannot parse returned error ${fail.reason.toHex} with sharedSecrets=$sharedSecrets: ${t.getMessage}")
213-
UnreadableRemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route))
216+
case Left(e) =>
217+
log.warning(s"cannot parse returned error: $e")
218+
UnreadableRemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e)
214219
}
215220
log.warning(s"too many failed attempts, failing the payment")
216221
myStop(c, Left(PaymentFailed(id, paymentHash, failures :+ failure)))
217-
case Failure(t) =>
218-
log.warning(s"cannot parse returned error: ${t.getMessage}, route=${route.printNodes()}")
219-
val failure = UnreadableRemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route))
222+
case Left(e) =>
223+
log.warning(s"cannot parse returned error: $e, route=${route.printNodes()}")
224+
val failure = UnreadableRemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e)
220225
retry(failure, d)
221-
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) =>
226+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) =>
222227
log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)")
223228
val failure = RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e)
224229
retry(failure, d)
225-
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
230+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
226231
log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)")
227232
val failure = RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e)
228233
if (Announcements.checkSig(failureMessage.update, nodeId)) {
@@ -249,7 +254,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
249254
goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ failure, ignore + nodeId)
250255
}
251256
}
252-
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
257+
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
253258
log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)")
254259
val failure = RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e)
255260
retry(failure, d)

0 commit comments

Comments
 (0)