What changed, and why it matters
This commit fixes a routing bug in the Eclair Lightning node. When a payment failed somewhere along the path, the software used to identify the failing node only by its public key. That caused confusion when the same node appeared more than once in a route (for example, in circular rebalancing payments), because the wrong occurrence could be blamed. The fix adds the failing node's position (index) in the route so the correct hop is identified. This is a correctness/reliability improvement rather than a direct theft-of-funds vulnerability, but misidentifying the failing hop could lead to poor routing decisions, unnecessary retries, or incorrect blacklisting of channels/nodes.
Treat as a reliability/correctness fix and include it in the next maintenance release. Reviewers should verify that `route.hops(index)` cannot throw an out-of-bounds exception for malformed failure packets, and that the index is consistently 1-based across all call sites. No emergency security patch is indicated, but operators running circular-rebalancing or trampoline payments should upgrade to avoid routing degradation.
Security signals we found
Failure attribution ambiguity when a node appears multiple times in a route
Potential incorrect hop/channel blacklisting due to node-id-only failure identification
Routing/liquidity learning could be poisoned by misattributed failures
No cryptographic weakness or direct fund-stealing primitive introduced
Fix is structural/correctness-oriented across Sphinx, payment lifecycle, and router
Evidence from the diff
The change adds an index: Int field to Sphinx.DecryptedFailurePacket and threads it through FailurePacket.decrypt, starting at 1 and incrementing on each recursive unwrap. Payment lifecycle logic now uses route.stopAt(index) and route.hops(index) instead of looking up hops by nodeId. Route.stopAt is rewritten to take an index rather than a node id. All pattern matches and serializers are updated to account for the new field. The primary risk addressed is ambiguity when a node’s public key appears multiple times in a route: previously the first matching hop was selected, which could mis-attribute failures and corrupt liquidity/ignore data used for retries.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scalaeclair-core/src/main/scala/fr/acinq/eclair/router/Router.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scalaeclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scalaInspect captured patch +89 / −91
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala
index f79f02e..97995c6 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala
@@ -273,9 +273,10 @@ object Sphinx extends Logging {
* A properly decrypted failure from a node in the route.
*
* @param originNode public key of the node that generated the failure.
+ * @param index position in the route of the node that generated the failure.
* @param failureMessage friendly failure message.
*/
- case class DecryptedFailurePacket(originNode: PublicKey, failureMessage: FailureMessage)
+ case class DecryptedFailurePacket(originNode: PublicKey, index: Int, failureMessage: FailureMessage)
/**
* The downstream failure could not be decrypted.
@@ -335,7 +336,7 @@ object Sphinx extends Logging {
* @return failure message if the origin of the packet could be identified and the packet decrypted, the unwrapped
* failure packet otherwise.
*/
- def decrypt(packet: ByteVector, attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret]): HtlcFailure = {
+ def decrypt(packet: ByteVector, attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret], index: Int = 1): HtlcFailure = {
sharedSecrets match {
case Nil => HtlcFailure(Nil, Left(CannotDecryptFailurePacket(packet, attribution_opt)))
case ss :: tail =>
@@ -343,8 +344,8 @@ object Sphinx extends Logging {
val attribution1_opt = attribution_opt.flatMap(Attribution.unwrap(_, packet1, ss.secret, sharedSecrets.length))
val um = generateKey("um", ss.secret)
val HtlcFailure(downstreamHoldTimes, failure) = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match {
- case Attempt.Successful(value) => HtlcFailure(Nil, Right(DecryptedFailurePacket(ss.remoteNodeId, value.value)))
- case _ => decrypt(packet1, attribution1_opt.map(_._2), tail)
+ case Attempt.Successful(value) => HtlcFailure(Nil, Right(DecryptedFailurePacket(ss.remoteNodeId, index, value.value)))
+ case _ => decrypt(packet1, attribution1_opt.map(_._2), tail, index + 1)
}
HtlcFailure(attribution1_opt.map(n => HoldTime(n._1, ss.remoteNodeId) +: downstreamHoldTimes).getOrElse(Nil), failure)
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
index f2e6871..f447fab 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
@@ -375,7 +375,7 @@ object PaymentFailedSummarySerializer extends ConvertClassSerializer[PaymentFail
val route = f.route.map(_.nodeId) ++ f.route.lastOption.map(_.nextNodeId)
val message = f match {
case LocalFailure(_, _, t) => t.getMessage
- case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, failureMessage)) => s"$origin returned: ${failureMessage.message}"
+ case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, _, failureMessage)) => s"$origin returned: ${failureMessage.message}"
case _: UnreadableRemoteFailure => "unreadable remote failure"
}
PaymentFailureSummaryJson(f.amount, route, message)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala
index 37721d3..eec22e5 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala
@@ -194,7 +194,7 @@ object PaymentFailure {
*/
def hasAlreadyFailedOnce(nodeId: PublicKey, failures: Seq[PaymentFailure]): Boolean =
failures
- .collectFirst { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, u: Update)) if origin == nodeId => u.update_opt }
+ .collectFirst { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, _, u: Update)) if origin == nodeId => u.update_opt }
.isDefined
/** Ignore the channel outgoing from the given nodeId in the given route. */
@@ -213,12 +213,12 @@ object PaymentFailure {
/** Update the set of nodes and channels to ignore in retries depending on the failure we received. */
def updateIgnored(failure: PaymentFailure, ignore: Ignore): Ignore = failure match {
- case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _)) if nodeId == hops.last.nextNodeId =>
+ case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, _)) if nodeId == hops.last.nextNodeId =>
// The failure came from the final recipient: the payment should be aborted without penalizing anyone in the route.
ignore
- case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(nodeId, _: Node)) =>
+ case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(nodeId, _, _: Node)) =>
ignore + nodeId
- case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
+ case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage: Update)) =>
if (failureMessage.update_opt.forall(update => Announcements.checkSig(update, nodeId))) {
val shouldIgnore = failureMessage match {
case _: TemporaryChannelFailure => true
@@ -235,7 +235,7 @@ object PaymentFailure {
// This node is fishy, it gave us a bad channel update signature, so let's filter it out.
ignore + nodeId
}
- case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _)) =>
+ case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, _)) =>
ignoreNodeOutgoingEdge(nodeId, hops, ignore)
case UnreadableRemoteFailure(_, hops, _, holdTimes) =>
// 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 {
// We're only interested in the last channel update received per channel.
val updates = failures.foldLeft(Map.empty[ShortChannelId, ChannelUpdate]) {
case (current, failure) => failure match {
- case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f: Update)) => f.update_opt match {
+ case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, _, f: Update)) => f.update_opt match {
case Some(update) => current.updated(update.shortChannelId, update)
case None => current
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala
index a30526e..d45e650 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala
@@ -76,7 +76,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto
case paymentResult: PaymentEvent =>
paymentResult match {
- case PaymentFailed(_, _, _ :+ RemoteFailure(_, _, DecryptedFailurePacket(targetNodeId, _: IncorrectOrUnknownPaymentDetails)), _) =>
+ case PaymentFailed(_, _, _ :+ RemoteFailure(_, _, DecryptedFailurePacket(targetNodeId, _, _: IncorrectOrUnknownPaymentDetails)), _) =>
log.info(s"payment probe successful to node=$targetNodeId")
case _ =>
log.info(s"payment probe failed with paymentResult=$paymentResult")
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala
index aa564e0..f1aacc9 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala
@@ -208,43 +208,40 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(request.amount, Nil, e, htlcFailure.holdTimes))).increment()
failure
}) match {
- case res@Right(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
+ case res@Right(Sphinx.DecryptedFailurePacket(_, index, failureMessage)) =>
// We have discovered some liquidity information with this payment: we update the router accordingly.
- val stoppedRoute = route.stopAt(nodeId)
+ val stoppedRoute = route.stopAt(index)
if (stoppedRoute.hops.length > 1) {
router ! Router.RouteCouldRelay(stoppedRoute)
}
failureMessage match {
case TemporaryChannelFailure(update_opt, _) =>
- route.hops.find(_.nodeId == nodeId) match {
- case Some(failingHop) =>
- val isLiquidityIssue = update_opt match {
- // If the relay parameters have changed, it's not necessarily a liquidity issue.
- case Some(update) => HopRelayParams.areSame(failingHop.params, HopRelayParams.FromAnnouncement(update), ignoreHtlcSize = true)
- case None => true
- }
- if (isLiquidityIssue) {
- router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop)
- }
- case _ => ()
+ val failingHop = route.hops(index)
+ val isLiquidityIssue = update_opt match {
+ // If the relay parameters have changed, it's not necessarily a liquidity issue.
+ case Some(update) => HopRelayParams.areSame(failingHop.params, HopRelayParams.FromAnnouncement(update), ignoreHtlcSize = true)
+ case None => true
+ }
+ if (isLiquidityIssue) {
+ router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop)
}
case _ => // other errors should not be used for liquidity issues
}
res
case res => res
}) match {
- case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == recipient.nodeId =>
+ case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) if nodeId == recipient.nodeId =>
// if destination node returns an error, we fail the payment immediately
log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)")
myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(request.amount, route.fullRoute, e))))
- case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if route.finalHop_opt.collect { case h: NodeHop if h.nodeId == nodeId => h }.nonEmpty =>
+ case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) if route.finalHop_opt.collect { case h: NodeHop if h.nodeId == nodeId => h }.nonEmpty =>
// if trampoline node returns an error, we fail the payment immediately
log.warning(s"received an error message from trampoline nodeId=$nodeId, failing the payment (failure=$failureMessage)")
myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(request.amount, route.fullRoute, e))))
case res if failures.size + 1 >= request.maxAttempts =>
// otherwise we never try more than maxAttempts, no matter the kind of error returned
val failure = res match {
- case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
+ case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) =>
log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)")
failureMessage match {
case failureMessage: Update => handleUpdate(nodeId, failureMessage, d)
@@ -261,11 +258,11 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
log.warning(s"cannot parse returned error: unwrapped=$unwrapped, route=${route.printNodes()}")
val failure = UnreadableRemoteFailure(request.amount, route.fullRoute, e, htlcFailure.holdTimes)
retry(failure, d)
- case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) =>
+ case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage: Node)) =>
log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)")
val failure = RemoteFailure(request.amount, route.fullRoute, e)
retry(failure, d)
- case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
+ case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage: Update)) =>
log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)")
val failure = RemoteFailure(request.amount, route.fullRoute, e)
if (failureMessage.update_opt.forall(update => Announcements.checkSig(update, nodeId))) {
@@ -292,7 +289,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore + nodeId)
}
}
- case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _: InvalidOnionBlinding)) =>
+ case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, _: InvalidOnionBlinding)) =>
// there was a failure inside the blinded route we used: we cannot know why it failed, so let's ignore it.
log.info(s"received an error coming from nodeId=$nodeId inside the blinded route, retrying with different blinded routes")
val failure = RemoteFailure(request.amount, route.fullRoute, e)
@@ -305,7 +302,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
router ! RouteRequest(self, nodeParams.nodeId, recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext))
goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore1)
}
- case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
+ case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) =>
log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)")
val failure = RemoteFailure(request.amount, route.fullRoute, e)
retry(failure, d)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala
index 28b8e95..dc5a133 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala
@@ -692,9 +692,9 @@ object Router {
def printChannels(): String = hops.map(_.shortChannelId).mkString("->")
- def stopAt(nodeId: PublicKey): Route = {
- val amountAtStop = hops.reverse.takeWhile(_.nextNodeId != nodeId).foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) }
- Route(amountAtStop, hops.takeWhile(_.nodeId != nodeId), None)
+ def stopAt(index: Int): Route = {
+ val amountAtStop = hops.drop(index).foldRight(amount) { case (hop, amount1) => amount1 + hop.fee(amount1) }
+ Route(amountAtStop, hops.take(index), None)
}
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala
index c4af434..af9c8de 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala
@@ -226,25 +226,25 @@ class SphinxSpec extends AnyFunSuite {
hex"0303030303030303030303030303030303030303030303030303030303030303"
).map(ByteVector32(_))
- val expected = DecryptedFailurePacket(publicKeys.head, InvalidOnionKey(ByteVector32.One))
+ val expected = DecryptedFailurePacket(publicKeys.head, 1, InvalidOnionKey(ByteVector32.One))
val packet1 = createAndWrap(sharedSecrets.head, expected.failureMessage)
assert(packet1.length == 292)
val Right(decrypted1) = FailurePacket.decrypt(packet1, None, Seq(0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure
- assert(decrypted1 == expected)
+ assert(decrypted1 == expected.copy(index = 1))
val packet2 = FailurePacket.wrap(packet1, sharedSecrets(1))
assert(packet2.length == 292)
val Right(decrypted2) = FailurePacket.decrypt(packet2, None, Seq(1, 0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure
- assert(decrypted2 == expected)
+ assert(decrypted2 == expected.copy(index = 2))
val packet3 = FailurePacket.wrap(packet2, sharedSecrets(2))
assert(packet3.length == 292)
val Right(decrypted3) = FailurePacket.decrypt(packet3, None, Seq(2, 1, 0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure
- assert(decrypted3 == expected)
+ assert(decrypted3 == expected.copy(index = 3))
}
test("decrypt invalid failure message") {
@@ -288,7 +288,7 @@ class SphinxSpec extends AnyFunSuite {
assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d")
// origin parses error packet and can see that it comes from node #4
- val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, None, sharedSecrets).failure
+ val Right(DecryptedFailurePacket(pubkey, 5, failure)) = FailurePacket.decrypt(error4, None, sharedSecrets).failure
assert(pubkey == publicKeys(4))
assert(failure == TemporaryNodeFailure())
}
@@ -331,7 +331,7 @@ class SphinxSpec extends AnyFunSuite {
assert(attribution4 == hex"84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706f42be9999a62ed49c7a81fc73c0b4a16419fd6d334532f40bf179dd19afec21bd8519d5e6ebc3802501ef373bc378eee1f14a6fc5fab5b697c91ce31d5922199d1b0ad5ee12176aacafc7c81d54bc5b8fb7e63f3bfd40a3b6e21f985340cbd1c124c7f85f0369d1aa86ebc66def417107a7861131c8bcd73e8946f4fb54bfac87a2dc15bd7af642f32ae583646141e8875ef81ec9083d7e32d5f135131eab7a43803360434100ff67087762bbe3d6afe2034f5746b8c50e0c3c20dd62a4c174c38b1df7365dccebc7f24f19406649fbf48981448abe5c858bbd4bef6eb983ae7a23e9309fb33b5e7c0522554e88ca04b1d65fc190947dead8c0ccd32932976537d869b5ca53ed4945bccafab2a014ea4cbdc6b0250b25be66ba0afff2ff19c0058c68344fd1b9c472567147525b13b1bc27563e61310110935cf89fda0e34d0575e2389d57bdf2869398ca2965f64a6f04e1d1c2edf2082b97054264a47824dd1a9691c27902b39d57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad")
// origin parses error packet and can see that it comes from node #4
- val HtlcFailure(holdTimes, Right(DecryptedFailurePacket(pubkey, parsedFailure))) = FailurePacket.decrypt(error4, Some(attribution4), sharedSecrets)
+ val HtlcFailure(holdTimes, Right(DecryptedFailurePacket(pubkey, 5, parsedFailure))) = FailurePacket.decrypt(error4, Some(attribution4), sharedSecrets)
assert(holdTimes == Seq(HoldTime(500 millisecond, publicKeys(0)), HoldTime(400 milliseconds, publicKeys(1)), HoldTime(300 milliseconds, publicKeys(2)), HoldTime(200 milliseconds, publicKeys(3)), HoldTime(100 milliseconds, publicKeys(4))))
assert(pubkey == publicKeys(4))
assert(parsedFailure == failure)
@@ -393,7 +393,7 @@ class SphinxSpec extends AnyFunSuite {
val attribution4 = Attribution.create(Some(attribution3), Some(error3), 500 milliseconds, sharedSecret0)
// origin parses error packet and can see that it comes from node #4
- val HtlcFailure(holdTimes, Right(DecryptedFailurePacket(pubkey, parsedFailure))) = FailurePacket.decrypt(error4, Some(attribution4), sharedSecrets)
+ val HtlcFailure(holdTimes, Right(DecryptedFailurePacket(pubkey, 5, parsedFailure))) = FailurePacket.decrypt(error4, Some(attribution4), sharedSecrets)
// We're missing attribution data from node #4 but we get hold times until node #3
assert(holdTimes == Seq(HoldTime(500 millisecond, publicKeys(0)), HoldTime(400 milliseconds, publicKeys(1)), HoldTime(300 milliseconds, publicKeys(2)), HoldTime(200 milliseconds, publicKeys(3))))
assert(pubkey == publicKeys(4))
@@ -452,7 +452,7 @@ class SphinxSpec extends AnyFunSuite {
assert(error4 == hex"751c187d145e5498306824f193c6bf9ed4a974fa85b3cc5d32d549ce494c1e7b3a06a19f8a9145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12dc942b5cf1db059d3e73d63967e464b5d5cfd4052de195387de93535e88a2e618e15a7c521d67ce2cc836c49118f205c99f18570504504221e337a29e2716fb28671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7cd13a7fe3f72a409bc3bdabfe0ba47a6d861e21f64d2fac706dab18b3e546df4")
// origin parses error packet and can see that it comes from node #4
- val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, None, sharedSecrets).failure
+ val Right(DecryptedFailurePacket(pubkey, 5, failure)) = FailurePacket.decrypt(error4, None, sharedSecrets).failure
assert(pubkey == publicKeys(4))
assert(failure == TemporaryNodeFailure())
}
@@ -472,7 +472,7 @@ class SphinxSpec extends AnyFunSuite {
val error2 = FailurePacket.wrap(error1, sharedSecret0)
// origin parses error packet and can see that it comes from node #2
- val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, None, sharedSecrets).failure
+ val Right(DecryptedFailurePacket(pubkey, 3, failure)) = FailurePacket.decrypt(error2, None, sharedSecrets).failure
assert(pubkey == publicKeys(2))
assert(failure == InvalidRealm())
}
@@ -493,7 +493,7 @@ class SphinxSpec extends AnyFunSuite {
assert(error2 == hex"c843486107187673b4586f5cdaad43ad84fbac03b39df51bbf9169b2bd682b409a855b2feb0545705f12eba9dbaecee84e328a9c2e4c3086bb1d0909d1f2e4f8a0e9c6be9541e94a849a0887756b984031dcb74d11c20d437a55daf3ee4109dea68ad74f9b742e7571d5e4d1b2ea4f7094787cf361b448a22a547ea85b833aae20f3ba79fb41c6636414c2092d41dd5328e2c1a1c754cb1f0d297628219f91fe946169f593ce7fce79103945d4d24adce46c083ab24757870356af55fcd3d22b9cfd83c45d409eb3081b218448d5dca3a201cf89ac88c9b66049d7c262b32081d3aba2098ea853bfa173ec23aa9253e083dfa881ef487b76780435c1b9f8a1d794557f0ac91d261d280bfb8513ad0c4dab0d7152eb9ee36ae63b8d384613684326d8735dc559f31cecb21b1d55bbcf7a281127adbedd0210b243325fd291cb82d443beec8f4b96aaee4b1a619724d7456b756d391e8fd3256d2b0766e39a435eb4d6d144c7fca1c73105710266e31120565444dfd6e9099e44d73a0f28419809577a267bbbc6671f723669d00c35c8e60fad88d89d4a7477a0c30f9839485197ed76338330f2ca00cf0e31c59da4eeebef977f429ad2c61acac35939866dac5b1df1c3c487ebaf961340c0c1dbc4bedebde7ee0633c3f480b7df265a3d90e78a4bcb9497f4228169fadb647e77afe6f43aa129286bb21767f6e75ac5c092473f99f2cf8b4e191f300c70b210e077a0385d483971bc0c66f5c119c0731a8753793ad12703d9cc5153eb1c8f25b71ee88a8d1d4433aa8f8277366c82111dbebfe0f548411588d54c3606742330d3d84a2f107df98d60995297de11672f6300b11444a04e252d69d8187772798afc6a9cd8b245a5ebd51bf0659f18c57daf1d1f724d2f15d524ab6902fb17a8fa6cee8e01df67735eac34bb0efc183dcb8d2a7cb401bd786c32a17f14c9d9ffc02b4f58c4ebab898a78b4913647d4cb5bafe6f7f27b5a256d1635c10f0ca71796610068c090c270c20bb18ec9d205e640d7655bdf5c9aeae20d7f9426eade0733c19d0aa577caf31f9d5be0a99ed0c509e84ccb555389ca69f09c3e66694a4ea2785f8d839d7dfff08b2c21aff89a023161cb1ebdd1e7a46d6380c0ddbc88eb3526e624fadcd222ecaa09566c2678158f933f03623299fec134a880d39a9d82ba2b29211e7787b3f32d478df856389a02cb68b66fc0dfc0b52353e7360f31e5457a6a9dd34512e912afeb5a92f3cbd3883b62c37e3ba5e4e8b688033150103c810740d130a5597c8a4a16311f50cfb3a919aac1e0a1096f20a14a536c55068ad38f40e62fc6f178b2fee67ca2cbd8afa29ef6c89b217aee02419ca26d59b604521a55e37c0a5a693fbc3ebcba23cd62479ddf62e5521847a2b4ac5e7686ef662c29cf8a8983660530942ee9a6c53b55e08af0b43467989693cefe6267fd524435152c01c9b93aebdec6146366a94162f99ac4c7157c15b988")
// origin parses error packet and can see that it comes from node #2
- val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, None, sharedSecrets).failure
+ val Right(DecryptedFailurePacket(pubkey, 3, failure)) = FailurePacket.decrypt(error2, None, sharedSecrets).failure
assert(pubkey == publicKeys(2))
assert(failure == InvalidRealm())
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala
index 5d00c1d..ba41fe6 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala
@@ -767,7 +767,7 @@ class PaymentsDbSpec extends AnyFunSuite {
db.updateOutgoingPayment(PaymentFailed(s3.id, s3.paymentHash, Nil, 310 unixms))
val ss3 = s3.copy(status = OutgoingPaymentStatus.Failed(Nil, 310 unixms))
assert(db.getOutgoingPayment(s3.id).contains(ss3))
- db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(s4.amount, Seq(hop_ab), new RuntimeException("woops")), RemoteFailure(s4.amount, Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer()))), 320 unixms))
+ db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(s4.amount, Seq(hop_ab), new RuntimeException("woops")), RemoteFailure(s4.amount, Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, 2, UnknownNextPeer()))), 320 unixms))
val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", List(HopSummary(alice, bob, Some(ShortChannelId(42)))), Some(alice)), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), Some(carol))), 320 unixms))
assert(db.getOutgoingPayment(s4.id).contains(ss4))
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala
index 66c4762..4af0971 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala
@@ -264,7 +264,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
assert(failed.id == paymentId)
assert(failed.paymentHash == htlc.paymentHash)
assert(failed.failures.nonEmpty)
- assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure()))
+ assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("C").nodeParams.nodeId, 1, PermanentChannelFailure()))
// we then generate enough blocks to confirm all delayed transactions
generateBlocks(25, Some(minerAddress))
val expectedTxCountC = 2 // C should have 2 recv transactions: its main output and the htlc timeout
@@ -317,7 +317,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
assert(failed.id == paymentId)
assert(failed.paymentHash == htlc.paymentHash)
assert(failed.failures.nonEmpty)
- assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure()))
+ assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("C").nodeParams.nodeId, 1, PermanentChannelFailure()))
// we then generate enough blocks to confirm all delayed transactions
generateBlocks(25, Some(minerAddress))
val expectedTxCountC = 2 // C should have 2 recv transactions: its main output and the htlc timeout
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala
index 5dd3038..0d8e890 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala
@@ -260,7 +260,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(failed.id == paymentId)
assert(failed.paymentHash == invoice.paymentHash)
assert(failed.failures.size == 1)
- assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(amount, getBlockHeight())))
+ assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, 3, IncorrectOrUnknownPaymentDetails(amount, getBlockHeight())))
assert(holdTimesRecorder.expectMsgType[Router.ReportedHoldTimes].holdTimes.map(_.remoteNodeId) == Seq("B", "C", "D").map(nodes(_).nodeParams.nodeId))
}
@@ -282,7 +282,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(failed.id == paymentId)
assert(failed.paymentHash == invoice.paymentHash)
assert(failed.failures.size == 1)
- assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000 msat, getBlockHeight())))
+ assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, 3, IncorrectOrUnknownPaymentDetails(100000000 msat, getBlockHeight())))
}
test("send an HTLC A->D with too much overpayment") {
@@ -302,7 +302,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(paymentId == failed.id)
assert(failed.paymentHash == invoice.paymentHash)
assert(failed.failures.size == 1)
- assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000 msat, getBlockHeight())))
+ assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, 3, IncorrectOrUnknownPaymentDetails(600000000 msat, getBlockHeight())))
}
test("send an HTLC A->D with a reasonable overpayment") {
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala
index fd1b540..43e337d 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala
@@ -277,7 +277,7 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience
// The channel update returned in failures doesn't leak the real scid.
val failure = sendFailingPayment(alice, carol, 40_000_000 msat, hints = List(List(carolHint)))
- val failureWithChannelUpdate = failure.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f: Update)) => f }
+ val failureWithChannelUpdate = failure.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, _, f: Update)) => f }
assert(failureWithChannelUpdate.length == 1)
assert(failureWithChannelUpdate.head.update_opt.map(_.shortChannelId).contains(bobAlias))
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala
index ececd0b..8646895 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala
@@ -194,7 +194,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectNoMessage(100 millis)
val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head
- childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(failingRoute.amount, failingRoute.fullRoute, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure())))))
+ childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(failingRoute.amount, failingRoute.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, PermanentChannelFailure())))))
// We retry ignoring the failing channel.
expectRouteRequest(router, nodeParams.nodeId, clearRecipient, routeParams.copy(randomize = true), allowMultiPart = true, ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_be, b, e))), paymentContext = Some(cfg.paymentContext))
router.send(payFsm, RouteResponse(Seq(Route(400_000 msat, hop_ac_1 :: hop_ce :: Nil, None), Route(600_000 msat, hop_ad :: hop_de :: Nil, None))))
@@ -225,12 +225,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectNoMessage(100 millis)
val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq
- childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure())))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure())))))
// When we retry, we ignore the failing node and we let the router know about the remaining pending route.
expectRouteRequest(router, nodeParams.nodeId, clearRecipient, routeParams.copy(randomize = true), Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))
// The second part fails while we're still waiting for new routes.
- childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure())))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure())))))
// We receive a response to our first request, but it's now obsolete: we re-sent a new route request that takes into
// account the latest failures.
router.send(payFsm, RouteResponse(Seq(Route(failedRoute1.amount, hop_ac_1 :: hop_ce :: Nil, None))))
@@ -342,7 +342,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
// B changed his fees and expiry after the invoice was issued.
val channelUpdate = channelUpdate_be.copy(feeBaseMsat = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24))
val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head
- childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(finalAmount, Some(channelUpdate)))))))
+ childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, FeeInsufficient(finalAmount, Some(channelUpdate)))))))
// We update the routing hints accordingly before requesting a new route.
val extraEdge1 = extraEdge.copy(feeBase = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24))
assert(router.expectMsgType[RouteRequest].target.extraEdges == Seq(extraEdge1))
@@ -366,7 +366,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
// NB: we need a channel update with a valid signature, otherwise we'll ignore the node instead of this specific channel.
val channelUpdate = Announcements.makeChannelUpdate(channelUpdate_be.chainHash, priv_b, e, channelUpdate_be.shortChannelId, channelUpdate_be.cltvExpiryDelta, channelUpdate_be.htlcMinimumMsat, channelUpdate_be.feeBaseMsat, channelUpdate_be.feeProportionalMillionths, channelUpdate_be.htlcMaximumMsat)
val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head
- childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(Some(channelUpdate)))))))
+ childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, TemporaryChannelFailure(Some(channelUpdate)))))))
// We update the routing hints accordingly before requesting a new route and ignore the channel.
val routeRequest = router.expectMsgType[RouteRequest]
assert(routeRequest.target.extraEdges == Seq(extraEdge))
@@ -387,7 +387,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
// The blinded route fails to relay the payment.
val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head
- childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, InvalidOnionBlinding(randomBytes32()))))))
+ childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, InvalidOnionBlinding(randomBytes32()))))))
// We retry and ignore that blinded route.
val routeRequest = router.expectMsgType[RouteRequest]
assert(routeRequest.ignore.channels.map(_.shortChannelId) == Set(hop_be.dummyId))
@@ -407,7 +407,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
{
val failures = Seq(
LocalFailure(finalAmount, Nil, ChannelUnavailable(randomBytes32())),
- RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48)))))),
+ RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, 2, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48)))))),
UnreadableRemoteFailure(finalAmount, Nil, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil)
)
val extraEdges1 = Seq(
@@ -419,10 +419,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
}
{
val failures = Seq(
- RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(1), 20 msat, 20, CltvExpiryDelta(20)))))),
- RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21)))))),
- RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22)))))),
- RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23)))))),
+ RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, 1, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(1), 20 msat, 20, CltvExpiryDelta(20)))))),
+ RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, 2, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21)))))),
+ RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, 1, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22)))))),
+ RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, 1, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23)))))),
)
val extraEdges1 = Seq(
ExtraEdge(a, b, ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23), defaultChannelUpdate.htlcMinimumMsat, Some(defaultChannelUpdate.htlcMaximumMsat)),
@@ -503,7 +503,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectMsgType[SendPaymentToRoute]
val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
- val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, IncorrectOrUnknownPaymentDetails(600_000 msat, BlockHeight(0)))))))
+ val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, 2, IncorrectOrUnknownPaymentDetails(600_000 msat, BlockHeight(0)))))))
assert(result.failures.length == 1)
val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics]
@@ -542,7 +542,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil))))
router.expectMsgType[RouteRequest]
- val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout())))))
+ val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout())))))
assert(result.failures.length == 2)
}
@@ -576,7 +576,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectMsgType[SendPaymentToRoute]
val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq
- childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, PaymentTimeout())))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout())))))
awaitCond(payFsm.stateName == PAYMENT_ABORTED)
sender.watch(payFsm)
@@ -613,7 +613,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED)
sender.watch(payFsm)
- childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, PaymentTimeout())))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout())))))
val result = sender.expectMsgType[PaymentSent]
assert(result.parts.length == 1 && result.parts.head.id == childId)
assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount
@@ -663,7 +663,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
assert(payFsm.stateData.asInstanceOf[PaymentAborted].pending.size == pending.size - 1)
// Fail all remaining child payments.
payFsm.stateData.asInstanceOf[PaymentAborted].pending.foreach(childId =>
- childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(pending(childId).amount, hop_ab_1 :: hop_be :: Nil, Sphinx.DecryptedFailurePacket(e, PaymentTimeout())))))
+ childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(pending(childId).amount, hop_ab_1 :: hop_be :: Nil, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout())))))
)
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala
index 101c8bc..9ea55f2 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala
@@ -533,7 +533,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg, ignore = Ignore(Set.empty, Set(ChannelDesc(update_bc.shortChannelId, b, c))))
routerForwarder.forward(routerFixture.router)
// we allow 2 tries, so we send a 2nd request to the router
- assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil)
+ assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, 1, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil)
}
test("payment failed (Update)") { routerFixture =>
@@ -581,7 +581,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
routerForwarder.forward(routerFixture.router)
// this time the router can't find a route: game over
- assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.amount, route2.hops, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil)
+ assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, 1, failure)) :: RemoteFailure(route2.amount, route2.hops, Sphinx.DecryptedFailurePacket(b, 1, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil)
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
routerForwarder.expectNoMessage(100 millis)
@@ -705,7 +705,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
routerForwarder.forward(router)
// we allow 2 tries, so we send a 2nd request to the router, which won't find another route
- assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil)
+ assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, 1, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil)
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
}
@@ -747,7 +747,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// Without the blinded route, the router cannot find a route to the recipient.
val failed = sender.expectMsgType[PaymentFailed]
- assert(failed.failures == Seq(RemoteFailure(defaultAmountMsat, route.hops ++ Seq(blindedHop), Sphinx.DecryptedFailurePacket(b, failure)), LocalFailure(defaultAmountMsat, Nil, RouteNotFound)))
+ assert(failed.failures == Seq(RemoteFailure(defaultAmountMsat, route.hops ++ Seq(blindedHop), Sphinx.DecryptedFailurePacket(b, 1, failure)), LocalFailure(defaultAmountMsat, Nil, RouteNotFound)))
awaitCond(nodeParams.db.payments.getOutgoingPayment(cfg.id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
}
@@ -879,14 +879,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
test("filter errors properly") { () =>
val failures = Seq(
LocalFailure(defaultAmountMsat, Nil, RouteNotFound),
- RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure())),
+ RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure())),
LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)),
LocalFailure(defaultAmountMsat, Nil, RouteNotFound)
)
val filtered = PaymentFailure.transformForUser(failures)
val expected = Seq(
LocalFailure(defaultAmountMsat, Nil, RouteNotFound),
- RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure())),
+ RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure())),
LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes))
)
assert(filtered == expected)
@@ -904,15 +904,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
(LocalFailure(defaultAmountMsat, NodeHop(a, b, CltvExpiryDelta(144), 0 msat) :: NodeHop(b, c, CltvExpiryDelta(144), 0 msat) :: Nil, RouteNotFound), Set.empty, Set.empty),
(LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal")), Set.empty, Set(ChannelDesc(scid_ab, a, b))),
// remote failure from final recipient -> all intermediate nodes behaved correctly
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(d, IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(42)))), Set.empty, Set.empty),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(d, 3, IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(42)))), Set.empty, Set.empty),
// remote failures from intermediate nodes -> depending on the failure, ignore either the failing node or its outgoing channel
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentNodeFailure())), Set(b), Set.empty),
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, TemporaryNodeFailure())), Set(c), Set.empty),
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure())), Set.empty, Set(ChannelDesc(scid_bc, b, c))),
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, UnknownNextPeer())), Set.empty, Set(ChannelDesc(scid_cd, c, d))),
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, Some(update_bc)))), Set.empty, Set.empty),
- (RemoteFailure(defaultAmountMsat, blindedRoute_abc, Sphinx.DecryptedFailurePacket(b, InvalidOnionBlinding(randomBytes32()))), Set.empty, Set(ChannelDesc(blindedHop_bc.dummyId, blindedHop_bc.nodeId, blindedHop_bc.nextNodeId))),
- (RemoteFailure(defaultAmountMsat, blindedRoute_abc, Sphinx.DecryptedFailurePacket(blindedHop_bc.resolved.route.blindedNodeIds(1), InvalidOnionBlinding(randomBytes32()))), Set.empty, Set(ChannelDesc(blindedHop_bc.dummyId, blindedHop_bc.nodeId, blindedHop_bc.nextNodeId))),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, PermanentNodeFailure())), Set(b), Set.empty),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, 2, TemporaryNodeFailure())), Set(c), Set.empty),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, PermanentChannelFailure())), Set.empty, Set(ChannelDesc(scid_bc, b, c))),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, 2, UnknownNextPeer())), Set.empty, Set(ChannelDesc(scid_cd, c, d))),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, FeeInsufficient(100 msat, Some(update_bc)))), Set.empty, Set.empty),
+ (RemoteFailure(defaultAmountMsat, blindedRoute_abc, Sphinx.DecryptedFailurePacket(b, 1, InvalidOnionBlinding(randomBytes32()))), Set.empty, Set(ChannelDesc(blindedHop_bc.dummyId, blindedHop_bc.nodeId, blindedHop_bc.nextNodeId))),
+ (RemoteFailure(defaultAmountMsat, blindedRoute_abc, Sphinx.DecryptedFailurePacket(blindedHop_bc.resolved.route.blindedNodeIds(1), 2, InvalidOnionBlinding(randomBytes32()))), Set.empty, Set(ChannelDesc(blindedHop_bc.dummyId, blindedHop_bc.nodeId, blindedHop_bc.nextNodeId))),
// unreadable remote failures -> blacklist all nodes except our direct peer, the final recipient, the last hop or nodes relaying attribution data
(UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set.empty, Set.empty),
(UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set(c), Set.empty),
@@ -929,8 +929,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
}
val failures = Seq(
- RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, TemporaryNodeFailure())),
- RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, UnknownNextPeer())),
+ RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, 2, TemporaryNodeFailure())),
+ RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, UnknownNextPeer())),
LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal"))
)
val ignore = PaymentFailure.updateIgnored(failures, Ignore.empty)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala
index cfac6c6..03dd707 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala
@@ -667,7 +667,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(fail_c.id == add_c.id)
val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason, None), None), add_b)
assert(fail_b.id == add_b.id)
- val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, fail_b.attribution_opt, payment.sharedSecrets).failure
+ val Right(Sphinx.DecryptedFailurePacket(failingNode, 4, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, fail_b.attribution_opt, payment.sharedSecrets).failure
assert(failingNode == e)
assert(decryptedFailure == failure)
}
@@ -698,7 +698,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(fail_b.id == add_b.id)
val htlcFailure = Sphinx.FailurePacket.decrypt(fail_b.reason, fail_b.attribution_opt, payment.sharedSecrets)
assert(htlcFailure.holdTimes == Seq(HoldTime(1200 milliseconds, b), HoldTime(600 milliseconds, c), HoldTime(400 milliseconds, d), HoldTime(0 milliseconds, e)))
- val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = htlcFailure.failure
+ val Right(Sphinx.DecryptedFailurePacket(failingNode, 4, decryptedFailure)) = htlcFailure.failure
assert(failingNode == e)
assert(decryptedFailure == failure)
}
@@ -734,7 +734,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(fail_c.id == add_c.id)
val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason, None), None), add_b)
assert(fail_b.id == add_b.id)
- val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, fail_b.attribution_opt, payment.sharedSecrets).failure
+ val Right(Sphinx.DecryptedFailurePacket(failingNode, 2, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, fail_b.attribution_opt, payment.sharedSecrets).failure
assert(failingNode == c)
assert(decryptedFailure == failure)
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala
index b571a8d..7559ce9 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala
@@ -538,7 +538,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
router.expectMessageType[RouteRequest]
// If we're having a hard time finding routes, raising the fee/cltv will likely help.
- val failures = LocalFailure(outgoingAmount, Nil, RouteNotFound) :: RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, PermanentNodeFailure())) :: LocalFailure(outgoingAmount, Nil, RouteNotFound) :: Nil
+ val failures = LocalFailure(outgoingAmount, Nil, RouteNotFound) :: RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, 1, PermanentNodeFailure())) :: LocalFailure(outgoingAmount, Nil, RouteNotFound) :: Nil
payFSM ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, failures)
incomingMultiPart.foreach { p =>
@@ -563,7 +563,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
val payFSM = mockPayFSM.expectMessageType[akka.actor.ActorRef]
router.expectMessageType[RouteRequest]
- val failures = RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(outgoingAmount, Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil) :: Nil
+ val failures = RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, 1, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(outgoingAmount, Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil) :: Nil
payFSM ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, failures)
incomingMultiPart.foreach { p =>
@@ -785,7 +785,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
validateOutgoingPayment(outgoingPayment)
// The outgoing payment fails, but it's not a liquidity issue.
- outgoingPayment.replyTo ! PaymentFailed(relayId, paymentHash, RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, TemporaryNodeFailure())) :: Nil)
+ outgoingPayment.replyTo ! PaymentFailed(relayId, paymentHash, RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, 1, TemporaryNodeFailure())) :: Nil)
incomingMultiPart.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]]
assert(fwd.channelId == p.add.channelId)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala
index 7e716db..cb901c8 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala
@@ -1935,7 +1935,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution {
val Success(route1 :: Nil) = findRoute(g, a, e, amount, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = hc, includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000))
assert(route2Ids(route1) == 1 :: 2 :: 3 :: Nil)
- val h = g.routeCouldRelay(route1.stopAt(c)).channelCouldNotSend(route1.hops.last, amount)
+ val h = g.routeCouldRelay(route1.stopAt(2)).channelCouldNotSend(route1.hops.last, amount)
val Success(route2 :: Nil) = findRoute(h, a, e, amount, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = hc, includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000))
assert(route2Ids(route2) == 1 :: 4 :: 5 :: Nil)
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.