diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 1681f2ecea..0ca7e58bfa 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -13,6 +13,16 @@ When using anchor outputs, allows propagating our local commitment transaction t This removes the need for increasing the commitment feerate based on mempool conditions, which ensures that channels won't be force-closed anymore when nodes disagree on the current feerate. +### Attributable failures + +Eclair now supports attributable failures which allow nodes to prove they are not the source of the failure and provide timing data. +Previously a failing node could choose not to report the failure and we would penalize all nodes of the route. +If all nodes of the route support attributable failures, we only need to penalize two nodes (there is still some uncertainty as to which of the two nodes is the failing one). +See https://github.com/lightning/bolts/pull/1044 for more details. + +Support is disabled by default as the spec is not yet final. +It can be enabled by setting `eclair.features.option_attributable_failure = optional` at the risk of being incompatible with the final spec. + ### API changes - `listoffers` now returns more details about each offer. diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index e241ad42c9..60bdfcf41a 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -72,6 +72,7 @@ eclair { option_shutdown_anysegwit = optional option_dual_fund = optional option_quiesce = optional + option_attributable_failure = disabled option_onion_messages = optional // This feature should only be enabled when acting as an LSP for mobile wallets. // When activating this feature, the peer-storage section should be customized to match desired SLAs. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index b7b60e287c..e2cf36d2a1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -270,6 +270,10 @@ object Features { val mandatory = 34 } + case object AttributableFailures extends Feature with InitFeature with NodeFeature with Bolt11Feature { + val rfcName = "option_attributable_failure" + val mandatory = 36 + } case object OnionMessages extends Feature with InitFeature with NodeFeature { val rfcName = "option_onion_messages" val mandatory = 38 @@ -373,6 +377,7 @@ object Features { ShutdownAnySegwit, DualFunding, Quiescence, + AttributableFailures, OnionMessages, ProvideStorage, ChannelType, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index e8e4bceb98..3b08caa8a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -215,7 +215,7 @@ final case class CMD_ADD_HTLC(replyTo: ActorRef, sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent { def id: Long } final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand -final case class CMD_FAIL_HTLC(id: Long, reason: FailureReason, delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand +final case class CMD_FAIL_HTLC(id: Long, reason: FailureReason, htlcReceivedAt_opt: Option[TimestampMilli], delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandWhenQuiescent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 3cf51d14a6..5a62648009 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -921,14 +921,14 @@ case class Commitments(params: ChannelParams, case None => Left(UnknownHtlcId(channelId, fulfill.id)) } - def sendFail(cmd: CMD_FAIL_HTLC, nodeSecret: PrivateKey): Either[ChannelException, (Commitments, HtlcFailureMessage)] = + def sendFail(cmd: CMD_FAIL_HTLC, nodeSecret: PrivateKey, useAttributableFailures: Boolean): Either[ChannelException, (Commitments, HtlcFailureMessage)] = getIncomingHtlcCrossSigned(cmd.id) match { case Some(htlc) if CommitmentChanges.alreadyProposed(changes.localChanges.proposed, htlc.id) => // we have already sent a fail/fulfill for this htlc Left(UnknownHtlcId(channelId, cmd.id)) case Some(htlc) => // we need the shared secret to build the error packet - OutgoingPaymentPacket.buildHtlcFailure(nodeSecret, cmd, htlc).map(fail => (copy(changes = changes.addLocalProposal(fail)), fail)) + OutgoingPaymentPacket.buildHtlcFailure(nodeSecret, useAttributableFailures, cmd, htlc).map(fail => (copy(changes = changes.addLocalProposal(fail)), fail)) case None => Left(UnknownHtlcId(channelId, cmd.id)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 0d59f8e626..a6cfa0f9fe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -501,7 +501,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall log.debug("delaying CMD_FAIL_HTLC with id={} for {}", c.id, delay) context.system.scheduler.scheduleOnce(delay, self, c.copy(delay_opt = None)) stay() - case None => d.commitments.sendFail(c, nodeParams.privateKey) match { + case None => d.commitments.sendFail(c, nodeParams.privateKey, nodeParams.features.hasFeature(Features.AttributableFailures)) match { case Right((commitments1, fail)) => if (c.commit) self ! CMD_SIGN() context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.aliases, commitments1, d.lastAnnouncement_opt)) @@ -668,7 +668,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall case PostRevocationAction.RejectHtlc(add) => log.debug("rejecting incoming htlc {}", add) // NB: we don't set commit = true, we will sign all updates at once afterwards. - self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(d.channelUpdate))), commit = true) + self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(d.channelUpdate))), Some(TimestampMilli.now()), commit = true) case PostRevocationAction.RelayFailure(result) => log.debug("forwarding {} to relayer", result) relayer ! result @@ -1498,7 +1498,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } case Event(c: CMD_FAIL_HTLC, d: DATA_SHUTDOWN) => - d.commitments.sendFail(c, nodeParams.privateKey) match { + d.commitments.sendFail(c, nodeParams.privateKey, nodeParams.features.hasFeature(Features.AttributableFailures)) match { case Right((commitments1, fail)) => if (c.commit) self ! CMD_SIGN() handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fail @@ -1617,11 +1617,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall case PostRevocationAction.RelayHtlc(add) => // BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown. log.debug("closing in progress: failing {}", add) - self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true) + self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), Some(TimestampMilli.now()), commit = true) case PostRevocationAction.RejectHtlc(add) => // BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown. log.debug("closing in progress: rejecting {}", add) - self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true) + self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), Some(TimestampMilli.now()), commit = true) case PostRevocationAction.RelayFailure(result) => log.debug("forwarding {} to relayer", result) relayer ! result @@ -1861,7 +1861,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall case Event(c: HtlcSettlementCommand, d: DATA_CLOSING) => (c match { case c: CMD_FULFILL_HTLC => d.commitments.sendFulfill(c) - case c: CMD_FAIL_HTLC => d.commitments.sendFail(c, nodeParams.privateKey) + case c: CMD_FAIL_HTLC => d.commitments.sendFail(c, nodeParams.privateKey, nodeParams.features.hasFeature(Features.AttributableFailures)) case c: CMD_FAIL_MALFORMED_HTLC => d.commitments.sendFailMalformed(c) }) match { case Right((commitments1, _)) => 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 1fdfa1bab1..c1e817b9aa 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 @@ -23,8 +23,10 @@ import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.Attempt import scodec.bits.ByteVector +import scodec.codecs.uint32 import scala.annotation.tailrec +import scala.concurrent.duration.{DurationLong, FiniteDuration} import scala.util.{Failure, Success, Try} /** @@ -282,24 +284,28 @@ object Sphinx extends Logging { */ case class CannotDecryptFailurePacket(unwrapped: ByteVector) + case class HoldTime(duration: FiniteDuration, remoteNodeId: PublicKey) + + case class HtlcFailure(holdTimes: Seq[HoldTime], failure: Either[CannotDecryptFailurePacket, DecryptedFailurePacket]) + object FailurePacket { /** - * Create a failure packet that will be returned to the sender. + * Create a failure packet that needs to be wrapped before being returned to the sender. * Each intermediate hop will add a layer of encryption and forward to the previous hop. * Note that malicious intermediate hops may drop the packet or alter it (which breaks the mac). * * @param sharedSecret destination node's shared secret that was computed when the original onion for the HTLC * was created or forwarded: see OnionPacket.create() and OnionPacket.wrap(). * @param failure failure message. - * @return a failure packet that can be sent to the destination node. + * @return a failure packet that still needs to be wrapped before being sent to the destination node. */ def create(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { val um = generateKey("um", sharedSecret) val packet = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).encode(failure).require.toByteVector logger.debug(s"um key: $um") logger.debug(s"raw error packet: ${packet.toHex}") - wrap(packet, sharedSecret) + packet } /** @@ -322,25 +328,108 @@ object Sphinx extends Logging { * it was sent by the corresponding node. * Note that malicious nodes in the route may have altered the packet, triggering a decryption failure. * - * @param packet failure packet. - * @param sharedSecrets nodes shared secrets. + * @param packet failure packet. + * @param attribution_opt attribution data for this failure packet. + * @param sharedSecrets nodes shared secrets. * @return failure message if the origin of the packet could be identified and the packet decrypted, the unwrapped * failure packet otherwise. */ - @tailrec - def decrypt(packet: ByteVector, sharedSecrets: Seq[SharedSecret]): Either[CannotDecryptFailurePacket, DecryptedFailurePacket] = { + def decrypt(packet: ByteVector, attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret], hopIndex: Int = 0): HtlcFailure = { sharedSecrets match { - case Nil => Left(CannotDecryptFailurePacket(packet)) + case Nil => HtlcFailure(Nil, Left(CannotDecryptFailurePacket(packet))) case ss :: tail => val packet1 = wrap(packet, ss.secret) + val attribution1_opt = attribution_opt.flatMap(Attribution.unwrap(_, packet1, ss.secret, hopIndex)) val um = generateKey("um", ss.secret) - FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match { - case Attempt.Successful(value) => Right(DecryptedFailurePacket(ss.remoteNodeId, value.value)) - case _ => decrypt(packet1, tail) + 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, hopIndex + 1) } + HtlcFailure(attribution1_opt.map(n => HoldTime(n._1, ss.remoteNodeId) +: downstreamHoldTimes).getOrElse(Nil), failure) } } + /** + * Attribution data is added to the failure packet and prevents a node from evading responsibility for its failures. + * Nodes that relay attribution data can prove that they are not the erring node and in case the erring node tries + * to hide, there will only be at most two nodes that can be the erring node (the last one to send attribution data + * and the one after it). + * It also adds timing data for each node on the path. + * https://github.com/lightning/bolts/pull/1044 + */ + object Attribution { + val maxNumHops = 20 + val holdTimeLength = 4 + val hmacLength = 4 // HMACs are truncated to 4 bytes to save space + val totalLength = maxNumHops * holdTimeLength + maxNumHops * (maxNumHops + 1) / 2 * hmacLength // = 920 + + private def cipher(bytes: ByteVector, sharedSecret: ByteVector32): ByteVector = { + val key = generateKey("ammagext", sharedSecret) + val stream = generateStream(key, totalLength) + bytes xor stream + } + + /** + * Get the HMACs from the attribution data. + * The layout of the attribution data is as follows (using maxNumHops = 3 for conciseness): + * holdTime(0) ++ holdTime(1) ++ holdTime(2) ++ + * hmacs(0)(0) ++ hmacs(0)(1) ++ hmacs(0)(2) ++ + * hmacs(1)(0) ++ hmacs(1)(1) ++ + * hmacs(2)(0) + * + * Where `hmac(i)(j)` is the hmac added by node `i` (counted from the node that built the attribution data), + * assuming it is `maxNumHops - 1 - i - j` hops away from the erring node. + */ + private def getHmacs(bytes: ByteVector): Seq[Seq[ByteVector]] = + (0 until maxNumHops).map(i => (0 until (maxNumHops - i)).map(j => { + val start = maxNumHops * holdTimeLength + (maxNumHops * i - (i * (i - 1)) / 2 + j) * hmacLength + bytes.slice(start, start + hmacLength) + })) + + /** + * Computes the HMACs for the node that is `minNumHop` hops away from us. Hence we only compute `maxNumHops - minNumHop` HMACs. + * HMACs are truncated to 4 bytes to save space. An attacker has only one try to guess the HMAC so 4 bytes should be enough. + */ + private def computeHmacs(mac: Mac32, failurePacket: ByteVector, holdTimes: ByteVector, hmacs: Seq[Seq[ByteVector]], minNumHop: Int): Seq[ByteVector] = { + (minNumHop until maxNumHops).map(i => { + val y = maxNumHops - i + mac.mac(failurePacket ++ + holdTimes.take(y * holdTimeLength) ++ + ByteVector.concat((0 until y - 1).map(j => hmacs(j)(i)))).bytes.take(hmacLength) + }) + } + + /** + * Create attribution data to send with the failure packet + * + * @param failurePacket the failure packet before being wrapped + */ + def create(previousAttribution_opt: Option[ByteVector], failurePacket: ByteVector, holdTime: FiniteDuration, sharedSecret: ByteVector32): ByteVector = { + val previousAttribution = previousAttribution_opt.getOrElse(ByteVector.low(totalLength)) + val previousHmacs = getHmacs(previousAttribution).dropRight(1).map(_.drop(1)) + val mac = Hmac256(generateKey("um", sharedSecret)) + val holdTimes = uint32.encode(holdTime.toMillis).require.bytes ++ previousAttribution.take((maxNumHops - 1) * holdTimeLength) + val hmacs = computeHmacs(mac, failurePacket, holdTimes, previousHmacs, 0) +: previousHmacs + cipher(holdTimes ++ ByteVector.concat(hmacs.map(ByteVector.concat(_))), sharedSecret) + } + + /** + * Unwrap one hop of attribution data + * @return a pair with the hold time for this hop and the attribution data for the next hop, or None if the attribution data was invalid + */ + def unwrap(encrypted: ByteVector, failurePacket: ByteVector, sharedSecret: ByteVector32, minNumHop: Int): Option[(FiniteDuration, ByteVector)] = { + val bytes = cipher(encrypted, sharedSecret) + val holdTime = uint32.decode(bytes.take(holdTimeLength).bits).require.value.milliseconds + val hmacs = getHmacs(bytes) + val mac = Hmac256(generateKey("um", sharedSecret)) + if (computeHmacs(mac, failurePacket, bytes.take(maxNumHops * holdTimeLength), hmacs.drop(1), minNumHop) == hmacs.head.drop(minNumHop)) { + val unwrapped = bytes.slice(holdTimeLength, maxNumHops * holdTimeLength) ++ ByteVector.low(holdTimeLength) ++ ByteVector.concat((hmacs.drop(1) :+ Seq()).map(s => ByteVector.low(hmacLength) ++ ByteVector.concat(s))) + Some(holdTime, unwrapped) + } else { + None + } + } + } } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index f5fcdfc3fa..53ce18f826 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -250,7 +250,7 @@ object FailureSummary { def apply(f: PaymentFailure): FailureSummary = f match { case LocalFailure(_, route, t) => FailureSummary(FailureType.LOCAL, t.getMessage, route.map(h => HopSummary(h)).toList, route.headOption.map(_.nodeId)) case RemoteFailure(_, route, e) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.map(h => HopSummary(h)).toList, Some(e.originNode)) - case UnreadableRemoteFailure(_, route, _) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList, None) + case UnreadableRemoteFailure(_, route, _, _) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList, None) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index b8bf08e627..ec6e3aaa84 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -339,7 +339,7 @@ class Peer(val nodeParams: NodeParams, pending.proposed.find(_.htlc.id == msg.id) match { case Some(htlc) => val failure = msg match { - case msg: WillFailHtlc => FailureReason.EncryptedDownstreamFailure(msg.reason) + case msg: WillFailHtlc => FailureReason.EncryptedDownstreamFailure(msg.reason, msg.attribution_opt) case msg: WillFailMalformedHtlc => FailureReason.LocalFailure(createBadOnionFailure(msg.onionHash, msg.failureCode)) } htlc.createFailureCommands(Some(failure))(log).foreach { case (channelId, cmd) => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) } 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 c070d9fb7e..e5d3e642c0 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 @@ -19,6 +19,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.Sphinx.HoldTime import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig @@ -84,18 +85,18 @@ case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[Paym sealed trait PaymentRelayed extends PaymentEvent { val amountIn: MilliSatoshi val amountOut: MilliSatoshi - val startedAt: TimestampMilli + val receivedAt: TimestampMilli val settledAt: TimestampMilli } -case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, startedAt: TimestampMilli, settledAt: TimestampMilli) extends PaymentRelayed { +case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, receivedAt: TimestampMilli, settledAt: TimestampMilli) extends PaymentRelayed { override val timestamp: TimestampMilli = settledAt } case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing, nextTrampolineNodeId: PublicKey, nextTrampolineAmount: MilliSatoshi) extends PaymentRelayed { override val amountIn: MilliSatoshi = incoming.map(_.amount).sum override val amountOut: MilliSatoshi = outgoing.map(_.amount).sum - override val startedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) + override val receivedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) override val settledAt: TimestampMilli = outgoing.map(_.settledAt).maxOption.getOrElse(TimestampMilli.now()) override val timestamp: TimestampMilli = settledAt } @@ -103,7 +104,7 @@ case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: Payment case class OnTheFlyFundingPaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing) extends PaymentRelayed { override val amountIn: MilliSatoshi = incoming.map(_.amount).sum override val amountOut: MilliSatoshi = outgoing.map(_.amount).sum - override val startedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) + override val receivedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) override val settledAt: TimestampMilli = outgoing.map(_.settledAt).maxOption.getOrElse(TimestampMilli.now()) override val timestamp: TimestampMilli = settledAt } @@ -150,7 +151,7 @@ case class LocalFailure(amount: MilliSatoshi, route: Seq[Hop], t: Throwable) ext case class RemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure /** A remote node failed the payment but we couldn't decrypt the failure (e.g. a malicious node tampered with the message). */ -case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop], failurePacket: ByteVector) extends PaymentFailure +case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop], failurePacket: ByteVector, holdTimes: Seq[HoldTime]) extends PaymentFailure object PaymentFailure { @@ -235,13 +236,15 @@ object PaymentFailure { } case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _)) => ignoreNodeOutgoingEdge(nodeId, hops, ignore) - case UnreadableRemoteFailure(_, hops, _) => + 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. // We don't know which node is sending garbage, let's blacklist all nodes except: + // - the nodes that returned attribution data (except the last one) // - the one we are directly connected to: it would be too restrictive for retries // - the final recipient: they have no incentive to send garbage since they want that payment // - the introduction point of a blinded route: we don't want a node before the blinded path to force us to ignore that blinded path // - the trampoline node: we don't want a node before the trampoline node to force us to ignore that trampoline node - val blacklist = hops.collect { case hop: ChannelHop => hop }.map(_.nextNodeId).drop(1).dropRight(1).toSet + val blacklist = hops.collect { case hop: ChannelHop => hop }.map(_.nextNodeId).drop(1 max (holdTimes.length - 1)).dropRight(1).toSet ignore ++ blacklist case LocalFailure(_, hops, _) => hops.headOption match { case Some(hop: ChannelHop) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 9c22b0ecc5..d7f22e8aca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -25,28 +25,31 @@ import fr.acinq.eclair.router.Router.Route import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{InvoiceRoutingInfo, OutgoingBlindedPaths} import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, PerHopPayload} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, ShortChannelId, UInt64, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, ShortChannelId, TimestampMilli, UInt64, randomBytes32, randomKey} import scodec.bits.ByteVector import scodec.{Attempt, DecodeResult} +import scala.concurrent.duration.{DurationInt, FiniteDuration} import scala.util.{Failure, Success} /** * Created by t-bast on 08/10/2019. */ -sealed trait IncomingPaymentPacket +sealed trait IncomingPaymentPacket { + def receivedAt: TimestampMilli +} /** Helpers to handle incoming payment packets. */ object IncomingPaymentPacket { // @formatter:off /** We are the final recipient. */ - case class FinalPacket(add: UpdateAddHtlc, payload: FinalPayload) extends IncomingPaymentPacket + case class FinalPacket(add: UpdateAddHtlc, payload: FinalPayload, receivedAt: TimestampMilli) extends IncomingPaymentPacket /** We are an intermediate node. */ sealed trait RelayPacket extends IncomingPaymentPacket /** We must relay the payment to a direct peer. */ - case class ChannelRelayPacket(add: UpdateAddHtlc, payload: IntermediatePayload.ChannelRelay, nextPacket: OnionRoutingPacket) extends RelayPacket { + case class ChannelRelayPacket(add: UpdateAddHtlc, payload: IntermediatePayload.ChannelRelay, nextPacket: OnionRoutingPacket, receivedAt: TimestampMilli) extends RelayPacket { val amountToForward: MilliSatoshi = payload.amountToForward(add.amountMsat) val outgoingCltv: CltvExpiry = payload.outgoingCltv(add.cltvExpiry) val relayFeeMsat: MilliSatoshi = add.amountMsat - amountToForward @@ -58,9 +61,9 @@ object IncomingPaymentPacket { def outerPayload: FinalPayload.Standard def innerPayload: IntermediatePayload.NodeRelay } - case class RelayToTrampolinePacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.Standard, nextPacket: OnionRoutingPacket) extends NodeRelayPacket - case class RelayToNonTrampolinePacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.ToNonTrampoline) extends NodeRelayPacket - case class RelayToBlindedPathsPacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.ToBlindedPaths) extends NodeRelayPacket + case class RelayToTrampolinePacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.Standard, nextPacket: OnionRoutingPacket, receivedAt: TimestampMilli) extends NodeRelayPacket + case class RelayToNonTrampolinePacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.ToNonTrampoline, receivedAt: TimestampMilli) extends NodeRelayPacket + case class RelayToBlindedPathsPacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.ToBlindedPaths, receivedAt: TimestampMilli) extends NodeRelayPacket // @formatter:on case class DecodedOnionPacket(payload: TlvStream[OnionPaymentPayloadTlv], next_opt: Option[OnionRoutingPacket]) @@ -134,7 +137,7 @@ object IncomingPaymentPacket { decryptEncryptedRecipientData(add, privateKey, payload, encrypted.data).flatMap { case DecodedEncryptedRecipientData(blindedPayload, nextPathKey) => validateBlindedChannelRelayPayload(add, payload, blindedPayload, nextPathKey, nextPacket).flatMap { - case ChannelRelayPacket(_, payload, nextPacket) if payload.outgoing == Right(ShortChannelId.toSelf) => + case ChannelRelayPacket(_, payload, nextPacket, _) if payload.outgoing == Right(ShortChannelId.toSelf) => decrypt(add.copy(onionRoutingPacket = nextPacket, tlvStream = add.tlvStream.copy(records = Set(UpdateAddHtlcTlv.PathKey(nextPathKey)))), privateKey, features) case relayPacket => Right(relayPacket) } @@ -142,7 +145,7 @@ object IncomingPaymentPacket { case None if add.pathKey_opt.isDefined => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) case None => // We are not inside a blinded path: channel relay information is directly available. - IntermediatePayload.ChannelRelay.Standard.validate(payload).left.map(_.failureMessage).map(payload => ChannelRelayPacket(add, payload, nextPacket)) + IntermediatePayload.ChannelRelay.Standard.validate(payload).left.map(_.failureMessage).map(payload => ChannelRelayPacket(add, payload, nextPacket, TimestampMilli.now())) } case DecodedOnionPacket(payload, None) => // We are the final node for the outer onion, so we are either: @@ -215,7 +218,7 @@ object IncomingPaymentPacket { case payload if add.amountMsat < payload.paymentRelayData.paymentConstraints.minAmount => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) case payload if add.cltvExpiry > payload.paymentRelayData.paymentConstraints.maxCltvExpiry => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) case payload if !Features.areCompatible(Features.empty, payload.paymentRelayData.allowedFeatures) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) - case payload => Right(ChannelRelayPacket(add, payload, nextPacket)) + case payload => Right(ChannelRelayPacket(add, payload, nextPacket, TimestampMilli.now())) } } @@ -223,7 +226,7 @@ object IncomingPaymentPacket { FinalPayload.Standard.validate(payload).left.map(_.failureMessage).flatMap { case payload if add.amountMsat < payload.amount => Left(FinalIncorrectHtlcAmount(add.amountMsat)) case payload if add.cltvExpiry < payload.expiry => Left(FinalIncorrectCltvExpiry(add.cltvExpiry)) - case payload => Right(FinalPacket(add, payload)) + case payload => Right(FinalPacket(add, payload, TimestampMilli.now())) } } @@ -233,7 +236,7 @@ object IncomingPaymentPacket { case payload if payload.paymentConstraints_opt.exists(c => c.maxCltvExpiry < add.cltvExpiry) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) case payload if !Features.areCompatible(Features.empty, payload.allowedFeatures) => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) case payload if add.cltvExpiry < payload.expiry => Left(InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket))) - case payload => Right(FinalPacket(add, payload)) + case payload => Right(FinalPacket(add, payload, TimestampMilli.now())) } } @@ -249,7 +252,7 @@ object IncomingPaymentPacket { // We merge contents from the outer and inner payloads. // We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless). val trampolinePacket = outerPayload.records.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet) - Right(FinalPacket(add, FinalPayload.Standard.createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket))) + Right(FinalPacket(add, FinalPayload.Standard.createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata, trampolinePacket), TimestampMilli.now())) } } } @@ -260,7 +263,7 @@ object IncomingPaymentPacket { IntermediatePayload.NodeRelay.Standard.validate(innerPayload).left.map(_.failureMessage).flatMap { case _ if add.amountMsat < outerPayload.amount => Left(FinalIncorrectHtlcAmount(add.amountMsat)) case _ if add.cltvExpiry != outerPayload.expiry => Left(FinalIncorrectCltvExpiry(add.cltvExpiry)) - case innerPayload => Right(RelayToTrampolinePacket(add, outerPayload, innerPayload, next)) + case innerPayload => Right(RelayToTrampolinePacket(add, outerPayload, innerPayload, next, TimestampMilli.now())) } } } @@ -270,7 +273,7 @@ object IncomingPaymentPacket { IntermediatePayload.NodeRelay.ToNonTrampoline.validate(innerPayload).left.map(_.failureMessage).flatMap { case _ if add.amountMsat < outerPayload.amount => Left(FinalIncorrectHtlcAmount(add.amountMsat)) case _ if add.cltvExpiry != outerPayload.expiry => Left(FinalIncorrectCltvExpiry(add.cltvExpiry)) - case innerPayload => Right(RelayToNonTrampolinePacket(add, outerPayload, innerPayload)) + case innerPayload => Right(RelayToNonTrampolinePacket(add, outerPayload, innerPayload, TimestampMilli.now())) } } } @@ -280,7 +283,7 @@ object IncomingPaymentPacket { IntermediatePayload.NodeRelay.ToBlindedPaths.validate(innerPayload).left.map(_.failureMessage).flatMap { case _ if add.amountMsat < outerPayload.amount => Left(FinalIncorrectHtlcAmount(add.amountMsat)) case _ if add.cltvExpiry != outerPayload.expiry => Left(FinalIncorrectCltvExpiry(add.cltvExpiry)) - case innerPayload => Right(RelayToBlindedPathsPacket(add, outerPayload, innerPayload)) + case innerPayload => Right(RelayToBlindedPathsPacket(add, outerPayload, innerPayload, TimestampMilli.now())) } } } @@ -345,12 +348,18 @@ object OutgoingPaymentPacket { } } - private def buildHtlcFailure(nodeSecret: PrivateKey, reason: FailureReason, add: UpdateAddHtlc): Either[CannotExtractSharedSecret, ByteVector] = { + private def buildHtlcFailure(nodeSecret: PrivateKey, useAttributableFailures: Boolean, reason: FailureReason, add: UpdateAddHtlc, holdTime: FiniteDuration): Either[CannotExtractSharedSecret, (ByteVector, TlvStream[UpdateFailHtlcTlv])] = { extractSharedSecret(nodeSecret, add).map(sharedSecret => { - reason match { - case FailureReason.EncryptedDownstreamFailure(packet) => Sphinx.FailurePacket.wrap(packet, sharedSecret) - case FailureReason.LocalFailure(failure) => Sphinx.FailurePacket.create(sharedSecret, failure) + val (packet, attribution) = reason match { + case FailureReason.EncryptedDownstreamFailure(packet, attribution) => (packet, attribution) + case FailureReason.LocalFailure(failure) => (Sphinx.FailurePacket.create(sharedSecret, failure), None) + } + val tlvs: TlvStream[UpdateFailHtlcTlv] = if (useAttributableFailures) { + TlvStream(UpdateFailHtlcTlv.AttributionData(Sphinx.FailurePacket.Attribution.create(attribution, packet, holdTime, sharedSecret))) + } else { + TlvStream.empty } + (Sphinx.FailurePacket.wrap(packet, sharedSecret), tlvs) }) } @@ -367,14 +376,18 @@ object OutgoingPaymentPacket { } } - def buildHtlcFailure(nodeSecret: PrivateKey, cmd: CMD_FAIL_HTLC, add: UpdateAddHtlc): Either[CannotExtractSharedSecret, HtlcFailureMessage] = { + def buildHtlcFailure(nodeSecret: PrivateKey, useAttributableFailures: Boolean, cmd: CMD_FAIL_HTLC, add: UpdateAddHtlc, now: TimestampMilli = TimestampMilli.now()): Either[CannotExtractSharedSecret, HtlcFailureMessage] = { add.pathKey_opt match { case Some(_) => // We are part of a blinded route and we're not the introduction node. val failure = InvalidOnionBlinding(Sphinx.hash(add.onionRoutingPacket)) Right(UpdateFailMalformedHtlc(add.channelId, add.id, failure.onionHash, failure.code)) case None => - buildHtlcFailure(nodeSecret, cmd.reason, add).map(encryptedReason => UpdateFailHtlc(add.channelId, cmd.id, encryptedReason)) + // If the htlcReceivedAt was lost (because the node restarted), we use a hold time of 0 which should be ignored by the payer. + val holdTime = cmd.htlcReceivedAt_opt.map(now - _).getOrElse(0 millisecond) + buildHtlcFailure(nodeSecret, useAttributableFailures, cmd.reason, add, holdTime).map { + case (encryptedReason, tlvs) => UpdateFailHtlc(add.channelId, cmd.id, encryptedReason, tlvs) + } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index c2dc342bba..a10e58698a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -53,13 +53,13 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP // NB: this is safe because this handler will be called from within an actor private var pendingPayments: Map[ByteVector32, (IncomingPayment, ActorRef)] = Map.empty - private def addHtlcPart(ctx: ActorContext, add: UpdateAddHtlc, payload: FinalPayload, payment: IncomingPayment): Unit = { + private def addHtlcPart(ctx: ActorContext, add: UpdateAddHtlc, payload: FinalPayload, payment: IncomingPayment, receivedAt: TimestampMilli): Unit = { pendingPayments.get(add.paymentHash) match { case Some((_, handler)) => - handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add) + handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add, receivedAt) case None => val handler = ctx.actorOf(MultiPartPaymentFSM.props(nodeParams, add.paymentHash, payload.totalAmount, ctx.self)) - handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add) + handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add, receivedAt) pendingPayments = pendingPayments + (add.paymentHash -> (payment, handler)) } } @@ -86,10 +86,10 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP val child = ctx.spawnAnonymous(GetIncomingPaymentActor(nodeParams, p, offerManager)) child ! GetIncomingPaymentActor.GetIncomingPayment(ctx.self) - case ProcessPacket(add, payload, payment_opt) if doHandle(add.paymentHash) => + case ProcessPacket(add, payload, payment_opt, receivedAt) if doHandle(add.paymentHash) => Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(add.paymentHash))) { payment_opt match { - case Some(payment) => validateStandardPayment(nodeParams, add, payload, payment) match { + case Some(payment) => validateStandardPayment(nodeParams, add, payload, payment, receivedAt) match { case Some(cmdFail) => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, Tags.FailureType(cmdFail)).increment() PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) @@ -100,7 +100,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP log.debug("received payment for amount={} totalAmount={} paymentMetadata={}", add.amountMsat, payload.totalAmount, payload.paymentMetadata.map(_.toHex).getOrElse("none")) Metrics.PaymentHtlcReceived.withTag(Tags.PaymentMetadataIncluded, payload.paymentMetadata.nonEmpty).increment() payload.paymentMetadata.foreach(metadata => ctx.system.eventStream.publish(PaymentMetadataReceived(add.paymentHash, metadata))) - addHtlcPart(ctx, add, payload, payment) + addHtlcPart(ctx, add, payload, payment, receivedAt) } case None => payload.paymentPreimage match { case Some(paymentPreimage) if nodeParams.features.hasFeature(Features.KeySend) => @@ -116,25 +116,25 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP val invoice = Bolt11Invoice(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.channelConf.minFinalExpiryDelta, paymentSecret = payload.paymentSecret, features = features) log.debug("generated fake invoice={} from amount={} (KeySend)", invoice.toString, amount) db.addIncomingPayment(invoice, paymentPreimage, PaymentType.KeySend) - ctx.self ! ProcessPacket(add, payload, Some(IncomingStandardPayment(invoice, paymentPreimage, PaymentType.KeySend, TimestampMilli.now(), IncomingPaymentStatus.Pending))) + ctx.self ! ProcessPacket(add, payload, Some(IncomingStandardPayment(invoice, paymentPreimage, PaymentType.KeySend, TimestampMilli.now(), IncomingPaymentStatus.Pending)), receivedAt) case _ => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment() - val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), Some(receivedAt), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) } } } - case ProcessBlindedPacket(add, payload, payment, maxRecipientPathFees) if doHandle(add.paymentHash) => + case ProcessBlindedPacket(add, payload, payment, maxRecipientPathFees, receivedAt) if doHandle(add.paymentHash) => Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(add.paymentHash))) { - validateBlindedPayment(nodeParams, add, payload, payment, maxRecipientPathFees) match { + validateBlindedPayment(nodeParams, add, payload, payment, maxRecipientPathFees, receivedAt) match { case Some(cmdFail) => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, Tags.FailureType(cmdFail)).increment() PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case None => val recipientPathFees = payload.amount - add.amountMsat log.debug("received payment for amount={} recipientPathFees={} totalAmount={}", add.amountMsat, recipientPathFees, payload.totalAmount) - addHtlcPart(ctx, add, payload, payment) + addHtlcPart(ctx, add, payload, payment, receivedAt) if (recipientPathFees > 0.msat) { // We've opted into deducing the blinded paths fees from the amount we receive for this payment. // We add an artificial payment part for those fees, otherwise we will never reach the total amount. @@ -143,9 +143,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP } } - case RejectPacket(add, failure) if doHandle(add.paymentHash) => + case RejectPacket(add, failure, receivedAt) if doHandle(add.paymentHash) => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, failure.getClass.getSimpleName).increment() - val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), Some(receivedAt), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case MultiPartPaymentFSM.MultiPartPaymentFailed(paymentHash, failure, parts) if doHandle(paymentHash) => @@ -154,7 +154,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP log.warning("payment with paidAmount={} failed ({})", parts.map(_.amount).sum, failure) pendingPayments.get(paymentHash).foreach { case (_, handler: ActorRef) => handler ! PoisonPill } parts.collect { - case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), commit = true)) + case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), Some(p.receivedAt), commit = true)) } pendingPayments = pendingPayments - paymentHash } @@ -174,7 +174,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(paymentHash))) { failure match { case Some(failure) => p match { - case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), commit = true)) + case p: MultiPartPaymentFSM.HtlcPart => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(failure), Some(p.receivedAt), commit = true)) case _: MultiPartPaymentFSM.RecipientBlindedPathFeePart => () } case None => p match { @@ -186,7 +186,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, commit = true)) ctx.system.eventStream.publish(received) } else { - val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), Some(p.receivedAt), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, cmdFail) } }) @@ -221,7 +221,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP parts.collect { case p: MultiPartPaymentFSM.HtlcPart => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment() - val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), Some(p.receivedAt), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, cmdFail) } } @@ -237,9 +237,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP object MultiPartHandler { // @formatter:off - private case class ProcessPacket(add: UpdateAddHtlc, payload: FinalPayload.Standard, payment_opt: Option[IncomingStandardPayment]) - private case class ProcessBlindedPacket(add: UpdateAddHtlc, payload: FinalPayload.Blinded, payment: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi) - private case class RejectPacket(add: UpdateAddHtlc, failure: FailureMessage) + private case class ProcessPacket(add: UpdateAddHtlc, payload: FinalPayload.Standard, payment_opt: Option[IncomingStandardPayment], receivedAt: TimestampMilli) + private case class ProcessBlindedPacket(add: UpdateAddHtlc, payload: FinalPayload.Blinded, payment: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi, receivedAt: TimestampMilli) + private case class RejectPacket(add: UpdateAddHtlc, failure: FailureMessage, receivedAt: TimestampMilli) case class DoFulfill(payment: IncomingPayment, success: MultiPartPaymentFSM.MultiPartPaymentSucceeded) case object GetPendingPayments @@ -371,28 +371,28 @@ object MultiPartHandler { nodeParams.db.payments.getIncomingPayment(packet.add.paymentHash) match { case Some(_: IncomingBlindedPayment) => context.log.info("rejecting non-blinded htlc #{} from channel {}: expected a blinded payment", packet.add.id, packet.add.channelId) - replyTo ! RejectPacket(packet.add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)) - case Some(payment: IncomingStandardPayment) => replyTo ! ProcessPacket(packet.add, payload, Some(payment)) - case None => replyTo ! ProcessPacket(packet.add, payload, None) + replyTo ! RejectPacket(packet.add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight), packet.receivedAt) + case Some(payment: IncomingStandardPayment) => replyTo ! ProcessPacket(packet.add, payload, Some(payment), packet.receivedAt) + case None => replyTo ! ProcessPacket(packet.add, payload, None, packet.receivedAt) } Behaviors.stopped case payload: FinalPayload.Blinded => offerManager ! OfferManager.ReceivePayment(context.self, packet.add.paymentHash, payload, packet.add.amountMsat) - waitForPayment(context, nodeParams, replyTo, packet.add, payload) + waitForPayment(context, nodeParams, replyTo, packet.add, payload, packet.receivedAt) } } } } } - private def waitForPayment(context: typed.scaladsl.ActorContext[Command], nodeParams: NodeParams, replyTo: ActorRef, add: UpdateAddHtlc, payload: FinalPayload.Blinded): Behavior[Command] = { + private def waitForPayment(context: typed.scaladsl.ActorContext[Command], nodeParams: NodeParams, replyTo: ActorRef, add: UpdateAddHtlc, payload: FinalPayload.Blinded, packetReceivedAt: TimestampMilli): Behavior[Command] = { Behaviors.receiveMessagePartial { case ProcessPayment(payment, maxRecipientPathFees) => - replyTo ! ProcessBlindedPacket(add, payload, payment, maxRecipientPathFees) + replyTo ! ProcessBlindedPacket(add, payload, payment, maxRecipientPathFees, packetReceivedAt) Behaviors.stopped case RejectPayment(reason) => context.log.info("rejecting blinded htlc #{} from channel {}: {}", add.id, add.channelId, reason) - replyTo ! RejectPacket(add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)) + replyTo ! RejectPacket(add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight), packetReceivedAt) Behaviors.stopped } } @@ -464,17 +464,17 @@ object MultiPartHandler { paymentAmountOk && paymentCltvOk && paymentStatusOk && paymentFeaturesOk } - private def validateStandardPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Standard, record: IncomingStandardPayment)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { + private def validateStandardPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Standard, record: IncomingStandardPayment, receivedAt: TimestampMilli)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { // We send the same error regardless of the failure to avoid probing attacks. - val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), Some(receivedAt), commit = true) val commonOk = validateCommon(nodeParams, add, payload, record) val secretOk = validatePaymentSecret(add, payload, record.invoice) if (commonOk && secretOk) None else Some(cmdFail) } - private def validateBlindedPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Blinded, record: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { + private def validateBlindedPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Blinded, record: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi, receivedAt: TimestampMilli)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { // We send the same error regardless of the failure to avoid probing attacks. - val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), Some(receivedAt), commit = true) val commonOk = validateCommon(nodeParams, add, payload, record) // The payer isn't aware of the blinded path fees if we decided to hide them. The HTLC amount will thus be smaller // than the onion amount, but should match when re-adding the blinded path fees. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala index 6bccb65f41..5e8f96f21f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala @@ -134,7 +134,7 @@ object MultiPartPaymentFSM { def totalAmount: MilliSatoshi } /** An incoming HTLC. */ - case class HtlcPart(totalAmount: MilliSatoshi, htlc: UpdateAddHtlc) extends PaymentPart { + case class HtlcPart(totalAmount: MilliSatoshi, htlc: UpdateAddHtlc, receivedAt: TimestampMilli) extends PaymentPart { override def paymentHash: ByteVector32 = htlc.paymentHash override def amount: MilliSatoshi = htlc.amountMsat } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index 72dd79ea51..ad6290b34e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -38,7 +38,7 @@ import fr.acinq.eclair.{EncodedNodeId, Features, InitFeature, Logs, NodeParams, import java.util.UUID import java.util.concurrent.TimeUnit -import scala.concurrent.duration.DurationLong +import scala.concurrent.duration.{DurationLong, FiniteDuration} import scala.util.Random object ChannelRelay { @@ -71,7 +71,7 @@ object ChannelRelay { parentPaymentId_opt = Some(relayId), // for a channel relay, parent payment id = relay id paymentHash_opt = Some(r.add.paymentHash), nodeAlias_opt = Some(nodeParams.alias))) { - val upstream = Upstream.Hot.Channel(r.add.removeUnknownTlvs(), TimestampMilli.now(), originNode) + val upstream = Upstream.Hot.Channel(r.add.removeUnknownTlvs(), r.receivedAt, originNode) val confidence = (r.add.endorsement + 0.5) / 8 new ChannelRelay(nodeParams, register, channels, r, upstream, confidence, context).start() } @@ -97,13 +97,13 @@ object ChannelRelay { } } - def translateRelayFailure(originHtlcId: Long, fail: HtlcResult.Fail): CMD_FAIL_HTLC = { + def translateRelayFailure(originHtlcId: Long, fail: HtlcResult.Fail, htlcReceivedAt_opt: Option[TimestampMilli]): CMD_FAIL_HTLC = { fail match { - case f: HtlcResult.RemoteFail => CMD_FAIL_HTLC(originHtlcId, FailureReason.EncryptedDownstreamFailure(f.fail.reason), commit = true) - case f: HtlcResult.RemoteFailMalformed => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(createBadOnionFailure(f.fail.onionHash, f.fail.failureCode)), commit = true) - case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true) - case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true) - case f: HtlcResult.DisconnectedBeforeSigned => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(TemporaryChannelFailure(Some(f.channelUpdate))), commit = true) + case f: HtlcResult.RemoteFail => CMD_FAIL_HTLC(originHtlcId, FailureReason.EncryptedDownstreamFailure(f.fail.reason, f.fail.attribution_opt), htlcReceivedAt_opt, commit = true) + case f: HtlcResult.RemoteFailMalformed => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(createBadOnionFailure(f.fail.onionHash, f.fail.failureCode)), htlcReceivedAt_opt, commit = true) + case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(PermanentChannelFailure()), htlcReceivedAt_opt, commit = true) + case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(PermanentChannelFailure()), htlcReceivedAt_opt, commit = true) + case f: HtlcResult.DisconnectedBeforeSigned => CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(TemporaryChannelFailure(Some(f.channelUpdate))), htlcReceivedAt_opt, commit = true) } } @@ -166,7 +166,7 @@ class ChannelRelay private(nodeParams: NodeParams, case WrappedPeerReadyResult(_: PeerReadyNotifier.PeerUnavailable) => Metrics.recordPaymentRelayFailed(Tags.FailureType.WakeUp, Tags.RelayType.Channel) context.log.info("rejecting htlc: failed to wake-up remote peer") - safeSendAndStop(r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + safeSendAndStop(r.add.channelId, makeCmdFailHtlc(r.add.id, UnknownNextPeer())) case WrappedPeerReadyResult(r: PeerReadyNotifier.PeerReady) => context.self ! DoRelay relay(Some(r.remoteFeatures), Seq.empty) @@ -203,7 +203,7 @@ class ChannelRelay private(nodeParams: NodeParams, Behaviors.receiveMessagePartial { case WrappedForwardFailure(Register.ForwardFailure(Register.Forward(_, channelId, _))) => context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${upstream.add.id}") - val cmdFail = CMD_FAIL_HTLC(upstream.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true) + val cmdFail = makeCmdFailHtlc(upstream.add.id, UnknownNextPeer()) Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel) safeSendAndStop(upstream.add.channelId, cmdFail) @@ -220,18 +220,18 @@ class ChannelRelay private(nodeParams: NodeParams, private def waitForAddSettled(): Behavior[Command] = Behaviors.receiveMessagePartial { case WrappedAddResponse(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill)) => - context.log.info("relaying fulfill to upstream, startedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, TimestampMilli.now(), confidence, upstream.receivedFrom, htlc.channelId) + context.log.info("relaying fulfill to upstream, receivedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, r.receivedAt, confidence, upstream.receivedFrom, htlc.channelId) Metrics.relayFulfill(confidence) val cmd = CMD_FULFILL_HTLC(upstream.add.id, fulfill.paymentPreimage, commit = true) - context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(upstream.amountIn, htlc.amountMsat, htlc.paymentHash, upstream.add.channelId, htlc.channelId, upstream.receivedAt, TimestampMilli.now())) + context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(upstream.amountIn, htlc.amountMsat, htlc.paymentHash, upstream.add.channelId, htlc.channelId, upstream.receivedAt, r.receivedAt)) recordRelayDuration(isSuccess = true) safeSendAndStop(upstream.add.channelId, cmd) case WrappedAddResponse(RES_ADD_SETTLED(_, htlc, fail: HtlcResult.Fail)) => - context.log.info("relaying fail to upstream, startedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, TimestampMilli.now(), confidence, upstream.receivedFrom, htlc.channelId) + context.log.info("relaying fail to upstream, receivedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, r.receivedAt, confidence, upstream.receivedFrom, htlc.channelId) Metrics.relayFail(confidence) Metrics.recordPaymentRelayFailed(Tags.FailureType.Remote, Tags.RelayType.Channel) - val cmd = translateRelayFailure(upstream.add.id, fail) + val cmd = translateRelayFailure(upstream.add.id, fail, Some(upstream.receivedAt)) recordRelayDuration(isSuccess = false) safeSendAndStop(upstream.add.channelId, cmd) } @@ -265,7 +265,7 @@ class ChannelRelay private(nodeParams: NodeParams, cmd match { // However, when the failure comes from us, we don't want to leak the unannounced channel by revealing // its channel_update: in that case, we always return a temporary node failure instead. - case cmd@CMD_FAIL_HTLC(_, FailureReason.LocalFailure(_: Update), _, _, _) => cmd.copy(reason = FailureReason.LocalFailure(TemporaryNodeFailure())) + case cmd@CMD_FAIL_HTLC(_, FailureReason.LocalFailure(_: Update), _, _, _, _) => cmd.copy(reason = FailureReason.LocalFailure(TemporaryNodeFailure())) case _ => cmd } case None => @@ -275,7 +275,7 @@ class ChannelRelay private(nodeParams: NodeParams, case Some(_) => // We are the introduction node: we add a delay to make it look like it could come from further downstream. val delay = Some(Random.nextLong(1000).millis) - CMD_FAIL_HTLC(cmd.id, FailureReason.LocalFailure(failure), delay, commit = true) + makeCmdFailHtlc(cmd.id, failure, delay) case None => // We are not the introduction node. CMD_FAIL_MALFORMED_HTLC(cmd.id, failure.onionHash, failure.code, commit = true) @@ -309,9 +309,9 @@ class ChannelRelay private(nodeParams: NodeParams, // Otherwise we return the error for the first channel tried. .getOrElse(previousFailures.head) .failure - CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(translateLocalError(error.t, error.channelUpdate)), commit = true) + makeCmdFailHtlc(r.add.id, translateLocalError(error.t, error.channelUpdate)) } else { - CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true) + makeCmdFailHtlc(r.add.id, UnknownNextPeer()) } walletNodeId_opt match { case Some(walletNodeId) if shouldAttemptOnTheFlyFunding(remoteFeatures_opt, previousFailures) => RelayNeedsFunding(walletNodeId, cmdFail) @@ -342,7 +342,7 @@ class ChannelRelay private(nodeParams: NodeParams, channel.channelUpdate, relayResult match { case _: RelaySuccess => "success" - case RelayFailure(CMD_FAIL_HTLC(_, FailureReason.LocalFailure(failureReason), _, _, _)) => failureReason + case RelayFailure(CMD_FAIL_HTLC(_, FailureReason.LocalFailure(failureReason), _, _, _, _)) => failureReason case other => other }) (channel, relayResult) @@ -389,7 +389,7 @@ class ChannelRelay private(nodeParams: NodeParams, case Some(fail) => RelayFailure(fail) case None if !update.channelFlags.isEnabled => - RelayFailure(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(ChannelDisabled(update.messageFlags, update.channelFlags, Some(update))), commit = true)) + RelayFailure(makeCmdFailHtlc(r.add.id, ChannelDisabled(update.messageFlags, update.channelFlags, Some(update)))) case None => val origin = Origin.Hot(addResponseAdapter.toClassic, upstream) RelaySuccess(outgoingChannel.channelId, CMD_ADD_HTLC(addResponseAdapter.toClassic, r.amountToForward, r.add.paymentHash, r.outgoingCltv, r.nextPacket, nextPathKey_opt, confidence, fundingFee_opt = None, origin, commit = true)) @@ -405,11 +405,11 @@ class ChannelRelay private(nodeParams: NodeParams, val expiryDeltaOk = update.cltvExpiryDelta <= r.expiryDelta || prevUpdate_opt.exists(_.cltvExpiryDelta <= r.expiryDelta) val feesOk = nodeFee(update.relayFees, r.amountToForward) <= r.relayFeeMsat || prevUpdate_opt.exists(u => nodeFee(u.relayFees, r.amountToForward) <= r.relayFeeMsat) if (!htlcMinimumOk) { - Some(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(AmountBelowMinimum(r.amountToForward, Some(update))), commit = true)) + Some(makeCmdFailHtlc(r.add.id, AmountBelowMinimum(r.amountToForward, Some(update)))) } else if (!expiryDeltaOk) { - Some(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(r.outgoingCltv, Some(update))), commit = true)) + Some(makeCmdFailHtlc(r.add.id, IncorrectCltvExpiry(r.outgoingCltv, Some(update)))) } else if (!feesOk) { - Some(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(update))), commit = true)) + Some(makeCmdFailHtlc(r.add.id, FeeInsufficient(r.add.amountMsat, Some(update)))) } else { None } @@ -429,6 +429,9 @@ class ChannelRelay private(nodeParams: NodeParams, featureOk && liquidityIssue && relayParamsOk } + private def makeCmdFailHtlc(originHtlcId: Long, failure: FailureMessage, delay_opt: Option[FiniteDuration] = None): CMD_FAIL_HTLC = + CMD_FAIL_HTLC(originHtlcId, FailureReason.LocalFailure(failure), Some(upstream.receivedAt), delay_opt, commit = true) + private def recordRelayDuration(isSuccess: Boolean): Unit = Metrics.RelayedPaymentDuration .withTag(Tags.Relay, Tags.RelayType.Channel) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index b976970999..f238d907f0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -107,7 +107,7 @@ object NodeRelay { }.toClassic val incomingPaymentHandler = context.actorOf(MultiPartPaymentFSM.props(nodeParams, paymentHash, totalAmountIn, mppFsmAdapters)) val nextPacket_opt = nodeRelayPacket match { - case IncomingPaymentPacket.RelayToTrampolinePacket(_, _, _, nextPacket) => Some(nextPacket) + case IncomingPaymentPacket.RelayToTrampolinePacket(_, _, _, nextPacket, _) => Some(nextPacket) case _: IncomingPaymentPacket.RelayToNonTrampolinePacket => None case _: IncomingPaymentPacket.RelayToBlindedPathsPacket => None } @@ -223,12 +223,12 @@ class NodeRelay private(nodeParams: NodeParams, case Relay(packet: IncomingPaymentPacket.NodeRelayPacket, originNode) => require(packet.outerPayload.paymentSecret == paymentSecret, "payment secret mismatch") context.log.debug("forwarding incoming htlc #{} from channel {} to the payment FSM", packet.add.id, packet.add.channelId) - handler ! MultiPartPaymentFSM.HtlcPart(packet.outerPayload.totalAmount, packet.add) - receiving(htlcs :+ Upstream.Hot.Channel(packet.add.removeUnknownTlvs(), TimestampMilli.now(), originNode), nextPayload, nextPacket_opt, handler) + handler ! MultiPartPaymentFSM.HtlcPart(packet.outerPayload.totalAmount, packet.add, packet.receivedAt) + receiving(htlcs :+ Upstream.Hot.Channel(packet.add.removeUnknownTlvs(), packet.receivedAt, originNode), nextPayload, nextPacket_opt, handler) case WrappedMultiPartPaymentFailed(MultiPartPaymentFSM.MultiPartPaymentFailed(_, failure, parts)) => context.log.warn("could not complete incoming multi-part payment (parts={} paidAmount={} failure={})", parts.size, parts.map(_.amount).sum, failure) Metrics.recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags.RelayType.Trampoline) - parts.collect { case p: MultiPartPaymentFSM.HtlcPart => rejectHtlc(p.htlc.id, p.htlc.channelId, p.amount, Some(failure)) } + parts.collect { case p: MultiPartPaymentFSM.HtlcPart => rejectHtlc(p.htlc.id, p.htlc.channelId, p.amount, p.receivedAt, Some(failure)) } stopping() case WrappedMultiPartPaymentSucceeded(MultiPartPaymentFSM.MultiPartPaymentSucceeded(_, parts)) => context.log.info("completed incoming multi-part payment with parts={} paidAmount={}", parts.size, parts.map(_.amount).sum) @@ -464,31 +464,31 @@ class NodeRelay private(nodeParams: NodeParams, private def rejectExtraHtlcPartialFunction: PartialFunction[Command, Behavior[Command]] = { case Relay(nodeRelayPacket, _) => - rejectExtraHtlc(nodeRelayPacket.add) + rejectExtraHtlc(nodeRelayPacket.add, nodeRelayPacket.receivedAt) Behaviors.same // NB: this message would be sent from the payment FSM which we stopped before going to this state, but all this is asynchronous. // We always fail extraneous HTLCs. They are a spec violation from the sender, but harmless in the relay case. // By failing them fast (before the payment has reached the final recipient) there's a good chance the sender won't lose any money. // We don't expect to relay pay-to-open payments. case WrappedMultiPartExtraPaymentReceived(extraPaymentReceived) => - rejectExtraHtlc(extraPaymentReceived.payment.htlc) + rejectExtraHtlc(extraPaymentReceived.payment.htlc, extraPaymentReceived.payment.receivedAt) Behaviors.same } - private def rejectExtraHtlc(add: UpdateAddHtlc): Unit = { + private def rejectExtraHtlc(add: UpdateAddHtlc, htlcReceivedAt: TimestampMilli): Unit = { context.log.warn("rejecting extra htlc #{} from channel {}", add.id, add.channelId) - rejectHtlc(add.id, add.channelId, add.amountMsat) + rejectHtlc(add.id, add.channelId, add.amountMsat, htlcReceivedAt) } - private def rejectHtlc(htlcId: Long, channelId: ByteVector32, amount: MilliSatoshi, failure: Option[FailureMessage] = None): Unit = { + private def rejectHtlc(htlcId: Long, channelId: ByteVector32, amount: MilliSatoshi, htlcReceivedAt: TimestampMilli, failure: Option[FailureMessage] = None): Unit = { val failureMessage = failure.getOrElse(IncorrectOrUnknownPaymentDetails(amount, nodeParams.currentBlockHeight)) - val cmd = CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(failureMessage), commit = true) + val cmd = CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(failureMessage), Some(htlcReceivedAt), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) } private def rejectPayment(upstream: Upstream.Hot.Trampoline, failure: Option[FailureMessage]): Unit = { Metrics.recordPaymentRelayFailed(failure.map(_.getClass.getSimpleName).getOrElse("Unknown"), Tags.RelayType.Trampoline) - upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, failure)) + upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, r.receivedAt, failure)) } private def fulfillPayment(upstream: Upstream.Hot.Trampoline, paymentPreimage: ByteVector32): Unit = upstream.received.foreach(r => { @@ -506,9 +506,9 @@ class NodeRelay private(nodeParams: NodeParams, context.system.eventStream ! EventStream.Publish(TrampolinePaymentRelayed(paymentHash, incoming, outgoing, paymentSent.recipientNodeId, paymentSent.recipientAmount)) } - private def recordRelayDuration(startedAt: TimestampMilli, isSuccess: Boolean): Unit = + private def recordRelayDuration(receivedAt: TimestampMilli, isSuccess: Boolean): Unit = Metrics.RelayedPaymentDuration .withTag(Tags.Relay, Tags.RelayType.Trampoline) .withTag(Tags.Success, isSuccess) - .record((TimestampMilli.now() - startedAt).toMillis, TimeUnit.MILLISECONDS) + .record((TimestampMilli.now() - receivedAt).toMillis, TimeUnit.MILLISECONDS) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala index 5b1b308f8c..3f0e56e67d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala @@ -100,14 +100,14 @@ object OnTheFlyFunding { // That's because we are directly connected to the wallet: the blinded path doesn't contain any other public nodes, // so we don't need to protect against probing. This allows us to return a more meaningful failure to the payer. val failure = failure_opt.getOrElse(FailureReason.LocalFailure(UnknownNextPeer())) - Seq(u.add.channelId -> CMD_FAIL_HTLC(u.add.id, failure, commit = true)) + Seq(u.add.channelId -> CMD_FAIL_HTLC(u.add.id, failure, Some(u.receivedAt), commit = true)) case u: Upstream.Hot.Trampoline => val failure = failure_opt match { case Some(f) => f match { case f: FailureReason.EncryptedDownstreamFailure => // In the trampoline case, we currently ignore downstream failures: we should add dedicated failures to // the BOLTs to better handle those cases. - Sphinx.FailurePacket.decrypt(f.packet, onionSharedSecrets) match { + Sphinx.FailurePacket.decrypt(f.packet, f.attribution_opt, onionSharedSecrets).failure match { case Left(Sphinx.CannotDecryptFailurePacket(_)) => log.warning("couldn't decrypt downstream on-the-fly funding failure") case Right(f) => @@ -118,7 +118,7 @@ object OnTheFlyFunding { } case None => FailureReason.LocalFailure(UnknownNextPeer()) } - u.received.map(_.add).map(add => add.channelId -> CMD_FAIL_HTLC(add.id, failure, commit = true)) + u.received.map(_.add).map(add => add.channelId -> CMD_FAIL_HTLC(add.id, failure, Some(u.receivedAt), commit = true)) } /** Create commands to fulfill all upstream HTLCs. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index b8f9c5810f..41978cf4d9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -136,7 +136,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(htlc.id, failure.onionHash, failure.code, commit = true) case None => - CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true) } channel ! cmd } else { @@ -269,7 +269,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val failure = InvalidOnionBlinding(ByteVector32.Zeroes) CMD_FAIL_MALFORMED_HTLC(originHtlcId, failure.onionHash, failure.code, commit = true) case None => - ChannelRelay.translateRelayFailure(originHtlcId, fail) + ChannelRelay.translateRelayFailure(originHtlcId, fail, None) } PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, cmd) case Upstream.Cold.Trampoline(originHtlcs) => @@ -278,7 +278,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment() // We don't bother decrypting the downstream failure to forward a more meaningful error upstream, it's // very likely that it won't be actionable anyway because of our node restart. - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index dd706ecf6e..0a9de9fd8f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams, RealShortChannelId} +import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams, RealShortChannelId, TimestampMilli} import grizzled.slf4j.Logging import scala.concurrent.Promise @@ -73,7 +73,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym case Right(r: IncomingPaymentPacket.NodeRelayPacket) => if (!nodeParams.enableTrampolinePayment) { log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=trampoline disabled") - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(RequiredNodeFeatureMissing()), commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(RequiredNodeFeatureMissing()), Some(r.receivedAt), commit = true)) } else { nodeRelayer ! NodeRelayer.Relay(r, originNode) } @@ -84,7 +84,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym // We are the introduction point of a blinded path: we add a non-negligible delay to make it look like it // could come from a downstream node. val delay = Some(500.millis + Random.nextLong(1500).millis) - CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(InvalidOnionBlinding(badOnion.onionHash)), delay, commit = true) + CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(InvalidOnionBlinding(badOnion.onionHash)), Some(TimestampMilli.now()), delay, commit = true) case _ => CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.code, commit = true) } @@ -92,7 +92,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case Left(failure) => log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=$failure") - val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), Some(TimestampMilli.now()), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) } 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 ab37eeaff0..41fbf99366 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 @@ -164,12 +164,13 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A private def handleRemoteFail(d: WaitingForComplete, fail: UpdateFailHtlc) = { import d._ - ((Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { + val htlcFailure = Sphinx.FailurePacket.decrypt(fail.reason, fail.attribution_opt, sharedSecrets) + ((htlcFailure.failure match { case success@Right(e) => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(request.amount, Nil, e))).increment() success case failure@Left(e) => - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(request.amount, Nil, e.unwrapped))).increment() + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(request.amount, Nil, e.unwrapped, htlcFailure.holdTimes))).increment() failure }) match { case res@Right(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => @@ -217,13 +218,13 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A RemoteFailure(request.amount, route.fullRoute, e) case Left(Sphinx.CannotDecryptFailurePacket(unwrapped)) => log.warning(s"cannot parse returned error ${fail.reason.toHex} with sharedSecrets=$sharedSecrets: unwrapped=$unwrapped") - UnreadableRemoteFailure(request.amount, route.fullRoute, unwrapped) + UnreadableRemoteFailure(request.amount, route.fullRoute, unwrapped, htlcFailure.holdTimes) } log.warning(s"too many failed attempts, failing the payment") myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ failure))) case Left(Sphinx.CannotDecryptFailurePacket(unwrapped)) => log.warning(s"cannot parse returned error: unwrapped=$unwrapped, route=${route.printNodes()}") - val failure = UnreadableRemoteFailure(request.amount, route.fullRoute, unwrapped) + val failure = UnreadableRemoteFailure(request.amount, route.fullRoute, unwrapped, htlcFailure.holdTimes) retry(failure, d) 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)") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala index 8f9d8d5cfa..bf09bd45fe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.wire.internal import akka.actor.ActorRef +import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.channel._ import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.FailureMessageCodecs._ @@ -34,14 +35,15 @@ object CommandCodecs { (("id" | int64) :: ("reason" | either(bool, varsizebinarydata, provide(TemporaryNodeFailure()).upcast[FailureMessage]).xmap[FailureReason]( { - case Left(packet) => FailureReason.EncryptedDownstreamFailure(packet) + case Left(packet) => FailureReason.EncryptedDownstreamFailure(packet, None) case Right(f) => FailureReason.LocalFailure(f) }, { - case FailureReason.EncryptedDownstreamFailure(packet) => Left(packet) + case FailureReason.EncryptedDownstreamFailure(packet, _) => Left(packet) case FailureReason.LocalFailure(f) => Right(f) } )) :: + ("htlcReceivedAt_opt" | provide(Option.empty[TimestampMilli])) :: ("delay_opt" | provide(Option.empty[FiniteDuration])) :: ("commit" | provide(false)) :: ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC] @@ -57,14 +59,24 @@ object CommandCodecs { (("id" | int64) :: ("reason" | either(bool8, varsizebinarydata, variableSizeBytes(uint16, failureMessageCodec)).xmap[FailureReason]( { - case Left(packet) => FailureReason.EncryptedDownstreamFailure(packet) + case Left(packet) => FailureReason.EncryptedDownstreamFailure(packet, None) case Right(f) => FailureReason.LocalFailure(f) }, { - case FailureReason.EncryptedDownstreamFailure(packet) => Left(packet) + case FailureReason.EncryptedDownstreamFailure(packet, _) => Left(packet) case FailureReason.LocalFailure(f) => Right(f) } )) :: + ("htlcReceivedAt_opt" | provide(Option.empty[TimestampMilli])) :: + // No need to delay commands after a restart, we've been offline which already created a random delay. + ("delay_opt" | provide(Option.empty[FiniteDuration])) :: + ("commit" | provide(false)) :: + ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC] + + private val cmdFailWithoutHoldTimeCodec: Codec[CMD_FAIL_HTLC] = + (("id" | int64) :: + ("reason" | failureReasonCodec) :: + ("htlcReceivedAt_opt" | provide(Option.empty[TimestampMilli])) :: // No need to delay commands after a restart, we've been offline which already created a random delay. ("delay_opt" | provide(Option.empty[FiniteDuration])) :: ("commit" | provide(false)) :: @@ -73,6 +85,7 @@ object CommandCodecs { private val cmdFailCodec: Codec[CMD_FAIL_HTLC] = (("id" | int64) :: ("reason" | failureReasonCodec) :: + ("htlcReceivedAt_opt" | optional(bool8, uint64overflow.as[TimestampMilli])) :: // No need to delay commands after a restart, we've been offline which already created a random delay. ("delay_opt" | provide(Option.empty[FiniteDuration])) :: ("commit" | provide(false)) :: @@ -87,7 +100,8 @@ object CommandCodecs { val cmdCodec: Codec[HtlcSettlementCommand] = discriminated[HtlcSettlementCommand].by(uint16) // NB: order matters! - .typecase(4, cmdFailCodec) + .typecase(5, cmdFailCodec) + .typecase(4, cmdFailWithoutHoldTimeCodec) .typecase(3, cmdFailEitherCodec) .typecase(2, cmdFailMalformedCodec) .typecase(1, cmdFailWithoutLengthCodec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala index 25aa721788..67e992217a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.eclair.crypto.Mac32 +import fr.acinq.eclair.crypto.{Mac32, Sphinx} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.failureMessageCodec import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelFlagsCodec, channelUpdateCodec, messageFlagsCodec, meteredLightningMessageCodec} @@ -36,7 +36,7 @@ import scodec.{Attempt, Codec, Err} sealed trait FailureReason object FailureReason { /** An encrypted failure coming from downstream which we should re-encrypt and forward upstream. */ - case class EncryptedDownstreamFailure(packet: ByteVector) extends FailureReason + case class EncryptedDownstreamFailure(packet: ByteVector, attribution_opt: Option[ByteVector]) extends FailureReason /** A local failure that should be encrypted for the node that created the payment onion. */ case class LocalFailure(failure: FailureMessage) extends FailureReason } @@ -168,8 +168,14 @@ object FailureMessageCodecs { fallback = unknownFailureMessageCodec.upcast[FailureMessage] ) + private val encryptedDownstreamFailure: Codec[FailureReason.EncryptedDownstreamFailure] = + (("packet" | varsizebinarydata) :: + ("attribution_opt" | optional(bool8, bytes(Sphinx.FailurePacket.Attribution.totalLength)))).as[FailureReason.EncryptedDownstreamFailure] + val failureReasonCodec: Codec[FailureReason] = discriminated[FailureReason].by(uint8) - .typecase(0, varsizebinarydata.as[FailureReason.EncryptedDownstreamFailure]) + // Order matters: latest codec comes first, then old codecs for backward compatibility + .typecase(2, encryptedDownstreamFailure) + .typecase(0, (varsizebinarydata :: provide[Option[ByteVector]](None)).as[FailureReason.EncryptedDownstreamFailure]) .typecase(1, variableSizeBytes(uint16, failureMessageCodec).as[FailureReason.LocalFailure]) private def failureOnionPayload(payloadAndPadLength: Int): Codec[FailureMessage] = Codec( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala index 7e608d36e5..716d2a124e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala @@ -18,11 +18,12 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tu16} -import scodec.{Attempt, Codec, Err} -import scodec.bits.HexStringSyntax +import scodec.bits.{ByteVector, HexStringSyntax} import scodec.codecs._ +import scodec.{Attempt, Codec, Err} /** * Created by t-bast on 19/07/2021. @@ -61,7 +62,13 @@ object UpdateFulfillHtlcTlv { sealed trait UpdateFailHtlcTlv extends Tlv object UpdateFailHtlcTlv { - val updateFailHtlcTlvCodec: Codec[TlvStream[UpdateFailHtlcTlv]] = tlvStream(discriminated[UpdateFailHtlcTlv].by(varint)) + case class AttributionData(data: ByteVector) extends UpdateFailHtlcTlv + + private val attributionData: Codec[AttributionData] = (("length" | constant(hex"fd0398")) :: ("data" | bytes(Sphinx.FailurePacket.Attribution.totalLength))).as[AttributionData] + + val updateFailHtlcTlvCodec: Codec[TlvStream[UpdateFailHtlcTlv]] = tlvStream(discriminated[UpdateFailHtlcTlv].by(varint) + .typecase(UInt64(1), attributionData) + ) } sealed trait UpdateFailMalformedHtlcTlv extends Tlv diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 0928d8ae35..cd9f4da73f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -471,7 +471,8 @@ object LightningMessageCodecs { val willFailHtlcCodec: Codec[WillFailHtlc] = ( ("id" | bytes32) :: ("paymentHash" | bytes32) :: - ("reason" | varsizebinarydata)).as[WillFailHtlc] + ("reason" | varsizebinarydata) :: + ("tlvStream" | UpdateFailHtlcTlv.updateFailHtlcTlvCodec)).as[WillFailHtlc] val willFailMalformedHtlcCodec: Codec[WillFailMalformedHtlc] = ( ("id" | bytes32) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 11034b42d0..d46a95c7a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -419,7 +419,9 @@ case class UpdateFulfillHtlc(channelId: ByteVector32, case class UpdateFailHtlc(channelId: ByteVector32, id: Long, reason: ByteVector, - tlvStream: TlvStream[UpdateFailHtlcTlv] = TlvStream.empty) extends HtlcMessage with UpdateMessage with HasChannelId with HtlcFailureMessage + tlvStream: TlvStream[UpdateFailHtlcTlv] = TlvStream.empty) extends HtlcMessage with UpdateMessage with HasChannelId with HtlcFailureMessage { + val attribution_opt: Option[ByteVector] = tlvStream.get[UpdateFailHtlcTlv.AttributionData].map(_.data) +} case class UpdateFailMalformedHtlc(channelId: ByteVector32, id: Long, @@ -685,7 +687,9 @@ object WillAddHtlc { } /** This message is similar to [[UpdateFailHtlc]], but for [[WillAddHtlc]]. */ -case class WillFailHtlc(id: ByteVector32, paymentHash: ByteVector32, reason: ByteVector) extends OnTheFlyFundingFailureMessage +case class WillFailHtlc(id: ByteVector32, paymentHash: ByteVector32, reason: ByteVector, tlvStream: TlvStream[UpdateFailHtlcTlv] = TlvStream.empty) extends OnTheFlyFundingFailureMessage { + val attribution_opt: Option[ByteVector] = tlvStream.get[UpdateFailHtlcTlv.AttributionData].map(_.data) +} /** This message is similar to [[UpdateFailMalformedHtlc]], but for [[WillAddHtlc]]. */ case class WillFailMalformedHtlc(id: ByteVector32, paymentHash: ByteVector32, onionHash: ByteVector32, failureCode: Int) extends OnTheFlyFundingFailureMessage diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala index 8d48db3a0b..a016e29b87 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala @@ -249,7 +249,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel // at this point the pending incoming htlc is waiting for a preimage assert(lcp4.htlcTxs(remainingHtlcOutpoint).isEmpty) - alice ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(UnknownNextPeer()), replyTo_opt = Some(probe.ref)) + alice ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(UnknownNextPeer()), None, replyTo_opt = Some(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) @@ -379,7 +379,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel } assert(!rcp3.isDone) - bob ! CMD_FAIL_HTLC(bobPendingHtlc.htlc.id, FailureReason.LocalFailure(UnknownNextPeer()), replyTo_opt = Some(probe.ref)) + bob ! CMD_FAIL_HTLC(bobPendingHtlc.htlc.id, FailureReason.LocalFailure(UnknownNextPeer()), None, replyTo_opt = Some(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 74d2f0d32f..e6208252d4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -200,8 +200,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc4.availableBalanceForSend == b) assert(bc4.availableBalanceForReceive == a - p - htlcOutputFee) - val cmdFail = CMD_FAIL_HTLC(0, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(p, BlockHeight(42)))) - val Right((bc5, fail: UpdateFailHtlc)) = bc4.sendFail(cmdFail, bob.underlyingActor.nodeParams.privateKey) + val cmdFail = CMD_FAIL_HTLC(0, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(p, BlockHeight(42))), None) + val Right((bc5, fail: UpdateFailHtlc)) = bc4.sendFail(cmdFail, bob.underlyingActor.nodeParams.privateKey, useAttributableFailures = false) assert(bc5.availableBalanceForSend == b) assert(bc5.availableBalanceForReceive == a - p - htlcOutputFee) // a's balance won't return to previous before she acknowledges the fail @@ -323,8 +323,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc8.availableBalanceForSend == b + p1 - p3) // as soon as we have the fulfill, the balance increases assert(bc8.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) - val cmdFail2 = CMD_FAIL_HTLC(1, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(p2, BlockHeight(42)))) - val Right((bc9, fail2: UpdateFailHtlc)) = bc8.sendFail(cmdFail2, bob.underlyingActor.nodeParams.privateKey) + val cmdFail2 = CMD_FAIL_HTLC(1, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(p2, BlockHeight(42))), None) + val Right((bc9, fail2: UpdateFailHtlc)) = bc8.sendFail(cmdFail2, bob.underlyingActor.nodeParams.privateKey, useAttributableFailures = false) assert(bc9.availableBalanceForSend == b + p1 - p3) assert(bc9.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) // a's balance won't return to previous before she acknowledges the fail diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 31ede8402a..8bd7d28e4e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -450,7 +450,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { } def failHtlc(id: Long, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { - s ! CMD_FAIL_HTLC(id, FailureReason.LocalFailure(TemporaryNodeFailure())) + s ! CMD_FAIL_HTLC(id, FailureReason.LocalFailure(TemporaryNodeFailure()), None) val fail = s2r.expectMsgType[UpdateFailHtlc] s2r.forward(r) eventually(assert(r.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.changes.remoteChanges.proposed.contains(fail))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala index 0804373785..5795657979 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala @@ -189,7 +189,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL val (preimage, add) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob) val cmd = c match { case FulfillHtlc => CMD_FULFILL_HTLC(add.id, preimage) - case FailHtlc => CMD_FAIL_HTLC(add.id, FailureReason.EncryptedDownstreamFailure(randomBytes(252))) + case FailHtlc => CMD_FAIL_HTLC(add.id, FailureReason.EncryptedDownstreamFailure(randomBytes(252), None), None) } crossSign(bob, alice, bob2alice, alice2bob) val sender = initiateQuiescence(f, sendInitialStfu) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 90eddc5d28..88e4597c5d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -1532,7 +1532,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val (_, htlc) = addHtlc(150000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure()), None) val fail = bob2alice.expectMsgType[UpdateFailHtlc] bob2alice.forward(alice) bob ! CMD_SIGN() @@ -1816,8 +1816,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val cmd = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure())) - val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, cmd, htlc) + val cmd = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure()), None) + val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, useAttributableFailures = false, cmd, htlc) assert(fail.id == htlc.id) bob ! cmd bob2alice.expectMsg(fail) @@ -1846,8 +1846,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val cmd = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure()), delay_opt = Some(50 millis)) - val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, cmd, htlc) + val cmd = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure()), None, delay_opt = Some(50 millis)) + val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, useAttributableFailures = false, cmd, htlc) assert(fail.id == htlc.id) bob ! cmd bob2alice.expectMsg(fail) @@ -1859,7 +1859,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) @@ -1879,7 +1879,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] // We cannot fail the HTLC, we must wait for the fulfill to be acked. - val c = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(TemporaryNodeFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(TemporaryNodeFailure()), None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), htlc.id))) } @@ -1889,7 +1889,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), None, replyTo_opt = Some(sender.ref)) sender.send(bob, c) // this will fail sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty) @@ -1943,7 +1943,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure()), None) val fail = bob2alice.expectMsgType[UpdateFailHtlc] // actual test begins @@ -2050,7 +2050,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // Bob receives a failure with a completely invalid onion error (missing mac) - bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.EncryptedDownstreamFailure(ByteVector.fill(561)(42))) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.EncryptedDownstreamFailure(ByteVector.fill(561)(42), None), None) val fail = bob2alice.expectMsgType[UpdateFailHtlc] assert(fail.id == htlc.id) // We propagate failure upstream (hopefully the sender knows how to unwrap them). diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index fd2ba85cde..5f75420333 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -650,7 +650,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // We simulate a pending failure on that HTLC. // Even if we get close to expiring upstream we shouldn't close the channel, because we have nothing to lose. - bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(0 msat, BlockHeight(0)))) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(0 msat, BlockHeight(0))), None) bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) bob2blockchain.expectNoMessage(250 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index e999591da5..033a39d04b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -244,7 +244,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_FAIL_HTLC") { f => import f._ val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - bob ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PermanentChannelFailure()), None) val fail = bob2alice.expectMsgType[UpdateFailHtlc] awaitCond(bob.stateData == initialState .modify(_.commitments.changes.localChanges.proposed).using(_ :+ fail) @@ -255,7 +255,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), None, replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) @@ -265,7 +265,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), None, replyTo_opt = Some(sender.ref)) sender.send(bob, c) // this will fail sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty) @@ -504,7 +504,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv RevokeAndAck (forward UpdateFailHtlc)") { f => import f._ - bob ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PermanentChannelFailure()), None) val fail = bob2alice.expectMsgType[UpdateFailHtlc] bob2alice.forward(alice) bob ! CMD_SIGN() 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 23dc635138..6ef01c8090 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 @@ -18,13 +18,15 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.crypto.Sphinx.FailurePacket import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedRoute, BlindedRouteDetails} import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, MilliSatoshiLong, ShortChannelId, UInt64, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, MilliSatoshiLong, ShortChannelId, UInt64, randomBytes, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ +import scala.concurrent.duration.DurationInt import scala.util.Success /** @@ -226,22 +228,22 @@ class SphinxSpec extends AnyFunSuite { val expected = DecryptedFailurePacket(publicKeys.head, InvalidOnionKey(ByteVector32.One)) - val packet1 = FailurePacket.create(sharedSecrets.head, expected.failureMessage) + val packet1 = createAndWrap(sharedSecrets.head, expected.failureMessage) assert(packet1.length == 292) - val Right(decrypted1) = FailurePacket.decrypt(packet1, Seq(0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))) + val Right(decrypted1) = FailurePacket.decrypt(packet1, None, Seq(0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure assert(decrypted1 == expected) val packet2 = FailurePacket.wrap(packet1, sharedSecrets(1)) assert(packet2.length == 292) - val Right(decrypted2) = FailurePacket.decrypt(packet2, Seq(1, 0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))) + val Right(decrypted2) = FailurePacket.decrypt(packet2, None, Seq(1, 0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure assert(decrypted2 == expected) val packet3 = FailurePacket.wrap(packet2, sharedSecrets(2)) assert(packet3.length == 292) - val Right(decrypted3) = FailurePacket.decrypt(packet3, Seq(2, 1, 0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))) + val Right(decrypted3) = FailurePacket.decrypt(packet3, None, Seq(2, 1, 0).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure assert(decrypted3 == expected) } @@ -254,11 +256,11 @@ class SphinxSpec extends AnyFunSuite { val packet = FailurePacket.wrap( FailurePacket.wrap( - FailurePacket.create(sharedSecrets.head, InvalidOnionPayload(UInt64(0), 0)), + createAndWrap(sharedSecrets.head, InvalidOnionPayload(UInt64(0), 0)), sharedSecrets(1)), sharedSecrets(2)) - assert(FailurePacket.decrypt(packet, Seq(0, 2, 1).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).isLeft) + assert(FailurePacket.decrypt(packet, None, Seq(0, 2, 1).map(i => SharedSecret(sharedSecrets(i), publicKeys(i)))).failure.isLeft) } test("last node replies with a short failure message (old reference test vector)") { @@ -274,7 +276,7 @@ class SphinxSpec extends AnyFunSuite { assert(lastPacket.isLastPacket) // node #4 want to reply with an error message - val error = FailurePacket.create(sharedSecret4, TemporaryNodeFailure()) + val error = createAndWrap(sharedSecret4, TemporaryNodeFailure()) assert(error == hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") val error1 = FailurePacket.wrap(error, sharedSecret3) assert(error1 == hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") @@ -286,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, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, None, sharedSecrets).failure assert(pubkey == publicKeys(4)) assert(failure == TemporaryNodeFailure()) } @@ -306,24 +308,100 @@ class SphinxSpec extends AnyFunSuite { // node #4 want to reply with an error message val failure = IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(800000), TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(34001), ByteVector.fill(300)(128))))) - val error = createCustomLengthFailurePacket(failure, sharedSecret4, 1024) + val failurePacket = createCustomLengthFailurePacket(failure, sharedSecret4, 1024) + val error = Sphinx.FailurePacket.wrap(failurePacket, sharedSecret4) assert(error == hex"146e94a9086dbbed6a0ab6932d00c118a7195dbf69b7d7a12b0e6956fc54b5e0a989f165b5f12fd45edd73a5b0c48630ff5be69500d3d82a29c0803f0a0679a6a073c33a6fb8250090a3152eba3f11a85184fa87b67f1b0354d6f48e3b342e332a17b7710f342f342a87cf32eccdf0afc2160808d58abb5e5840d2c760c538e63a6f841970f97d2e6fe5b8739dc45e2f7f5f532f227bcc2988ab0f9cc6d3f12909cd5842c37bc8c7608475a5ebbe10626d5ecc1f3388ad5f645167b44a4d166f87863fe34918cea25c18059b4c4d9cb414b59f6bc50c1cea749c80c43e2344f5d23159122ed4ab9722503b212016470d9610b46c35dbeebaf2e342e09770b38392a803bc9d2e7c8d6d384ffcbeb74943fe3f64afb2a543a6683c7db3088441c531eeb4647518cb41992f8954f1269fb969630944928c2d2b45593731b5da0c4e70d04a0a57afe4af42e99912fbb4f8883a5ecb9cb29b883cb6bfa0f4db2279ff8c6d2b56a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fcb4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0ecacb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b8285727735ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb2fe0329666477ee7cb7ebd6b88ad5add8b217188b1ca0fa13de1ec09cc674346875105be6e0e0d6c8928eb0df23c39a639e04e4aedf535c4e093f08b2c905a14f25c0c0fe47a5a1535ab9eae0d9d67bdd79de13a08d59ee05385c7ea4af1ad3248e61dd22f8990e9e99897d653dd7b1b1433a6d464ea9f74e377f2d8ce99ba7dbc753297644234d25ecb5bd528e2e2082824681299ac30c05354baaa9c3967d86d7c07736f87fc0f63e5036d47235d7ae12178ced3ae36ee5919c093a02579e4fc9edad2c446c656c790704bfc8e2c491a42500aa1d75c8d4921ce29b753f883e17c79b09ea324f1f32ddf1f3284cd70e847b09d90f6718c42e5c94484cc9cbb0df659d255630a3f5a27e7d5dd14fa6b974d1719aa98f01a20fb4b7b1c77b42d57fab3c724339d459ee4a1c6b5d3bd4e08624c786a257872acc9ad3ff62222f2265a658d9f2a007229a5293b67ec91c84c4b4407c228434bad8a815ca9b256c776bd2c9f") + val attribution = FailurePacket.Attribution.create(None, failurePacket, 1 millisecond, sharedSecret4) + assert(attribution == hex"d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb8cf5958e3f3f2a4365d1ec0df1d83c6a6221b5b7d1ff30156a2289a1d3ee559e7c7256bda444bb8e046f860e00b3a59a85e1e1a43de215fd5e6bf646a5deab97b1912c934e31b1cfd344764d6ca7e14ea7b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252") val error1 = FailurePacket.wrap(error, sharedSecret3) assert(error1 == hex"7512354d6a26781d25e65539772ba049b7ed7c530bf75ab7ef80cf974b978a07a1c3dabc61940011585323f70fa98cfa1d4c868da30b1f751e44a72d9b3f79809c8c51c9f0843daa8fe83587844fedeacb7348362003b31922cbb4d6169b2087b6f8d192d9cfe5363254cd1fde24641bde9e422f170c3eb146f194c48a459ae2889d706dc654235fa9dd20307ea54091d09970bf956c067a3bcc05af03c41e01af949a131533778bf6ee3b546caf2eabe9d53d0fb2e8cc952b7e0f5326a69ed2e58e088729a1d85971c6b2e129a5643f3ac43da031e655b27081f10543262cf9d72d6f64d5d96387ac0d43da3e3a03da0c309af121dcf3e99192efa754eab6960c256ffd4c546208e292e0ab9894e3605db098dc16b40f17c320aa4a0e42fc8b105c22f08c9bc6537182c24e32062c6cd6d7ec7062a0c2c2ecdae1588c82185cdc61d874ee916a7873ac54cddf929354f307e870011704a0e9fbc5c7802d6140134028aca0e78a7e2f3d9e5c7e49e20c3a56b624bfea51196ec9e88e4e56be38ff56031369f45f1e03be826d44a182f270c153ee0d9f8cf9f1f4132f33974e37c7887d5b857365c873cb218cbf20d4be3abdb2a2011b14add0a5672e01e5845421cf6dd6faca1f2f443757aae575c53ab797c2227ecdab03882bbbf4599318cefafa72fa0c9a0f5a51d13c9d0e5d25bfcfb0154ed25895260a9df8743ac188714a3f16960e6e2ff663c08bffda41743d50960ea2f28cda0bc3bd4a180e297b5b41c700b674cb31d99c7f2a1445e121e772984abff2bbe3f42d757ceeda3d03fb1ffe710aecabda21d738b1f4620e757e57b123dbc3c4aa5d9617dfa72f4a12d788ca596af14bea583f502f16fdc13a5e739afb0715424af2767049f6b9aa107f69c5da0e85f6d8c5e46507e14616d5d0b797c3dea8b74a1b12d4e47ba7f57f09d515f6c7314543f78b5e85329d50c5f96ee2f55bbe0df742b4003b24ccbd4598a64413ee4807dc7f2a9c0b92424e4ae1b418a3cdf02ea4da5c3b12139348aa7022cc8272a3a1714ee3e4ae111cffd1bdfd62c503c80bdf27b2feaea0d5ab8fe00f9cec66e570b00fd24b4a2ed9a5f6384f148a4d6325110a41ca5659ebc5b98721d298a52819b6fb150f273383f1c5754d320be428941922da790e17f482989c365c078f7f3ae100965e1b38c052041165295157e1a7c5b7a57671b842d4d85a7d971323ad1f45e17a16c4656d889fc75c12fc3d8033f598306196e29571e414281c5da19c12605f48347ad5b4648e371757cbe1c40adb93052af1d6110cfbf611af5c8fc682b7e2ade3bfca8b5c7717d19fc9f97964ba6025aebbc91a6671e259949dcf40984342118de1f6b514a7786bd4f6598ffbe1604cef476b2a4cb1343db608aca09d1d38fc23e98ee9c65e7f6023a8d1e61fd4f34f753454bd8e858c8ad6be6403edc599c220e03ca917db765980ac781e758179cd93983e9c1e769e4241d47c") + val attribution1 = FailurePacket.Attribution.create(Some(attribution), error, 2 milliseconds, sharedSecret3) + assert(attribution1 == hex"1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf36dee297079eb841391ccd3096da76461e314863b6412efe0ffe228d51c6097db10d3edb2e50ea679820613bfe9db11ba02920ab4c1f2a79890d997f1fc022f3ab78f0029cc6de0c90be74d55f4a99bf77a50e20f8d076fe61776190a61d2f41c408871c0279309cba3b60fcdc7efc4a0e90b47cb4a418fc78f362ecc7f15ebbce9f854c09c7be300ebc1a40a69d4c7cb7a19779b6905e82bec221a709c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb") val error2 = FailurePacket.wrap(error1, sharedSecret2) assert(error2 == hex"145bc1c63058f7204abbd2320d422e69fb1b3801a14312f81e5e29e6b5f4774cfed8a25241d3dfb7466e749c1b3261559e49090853612e07bd669dfb5f4c54162fa504138dabd6ebcf0db8017840c35f12a2cfb84f89cc7c8959a6d51815b1d2c5136cedec2e4106bb5f2af9a21bd0a02c40b44ded6e6a90a145850614fb1b0eef2a03389f3f2693bc8a755630fc81fff1d87a147052863a71ad5aebe8770537f333e07d841761ec448257f948540d8f26b1d5b66f86e073746106dfdbb86ac9475acf59d95ece037fba360670d924dce53aaa74262711e62a8fc9eb70cd8618fbedae22853d3053c7f10b1a6f75369d7f73c419baa7dbf9f1fc5895362dcc8b6bd60cca4943ef7143956c91992119bccbe1666a20b7de8a2ff30a46112b53a6bb79b763903ecbd1f1f74952fb1d8eb0950c504df31fe702679c23b463f82a921a2c931500ab08e686cffb2d87258d254fb17843959cccd265a57ba26c740f0f231bb76df932b50c12c10be90174b37d454a3f8b284c849e86578a6182c4a7b2e47dd57d44730a1be9fec4ad07287a397e28dce4fda57e9cdfdb2eb5afdf0d38ef19d982341d18d07a556bb16c1416f480a396f278373b8fd9897023a4ac506e65cf4c306377730f9c8ca63cf47565240b59c4861e52f1dab84d938e96fb31820064d534aca05fd3d2600834fe4caea98f2a748eb8f200af77bd9fbf46141952b9ddda66ef0ebea17ea1e7bb5bce65b6e71554c56dd0d4e14f4cf74c77a150776bf31e7419756c71e7421dc22efe9cf01de9e19fc8808d5b525431b944400db121a77994518d6025711cb25a18774068bba7faaa16d8f65c91bec8768848333156dcb4a08dfbbd9fef392da3e4de13d4d74e83a7d6e46cfe530ee7a6f711e2caf8ad5461ba8177b2ef0a518baf9058ff9156e6aa7b08d938bd8d1485a787809d7b4c8aed97be880708470cd2b2cdf8e2f13428cc4b04ef1f2acbc9562f3693b948d0aa94b0e6113cafa684f8e4a67dc431dfb835726874bef1de36f273f52ee694ec46b0700f77f8538067642a552968e866a72a3f2031ad116663ac17b172b446c5bc705b84777363a9a3fdc6443c07b2f4ef58858122168d4ebbaee920cefc312e1cea870ed6e15eec046ab2073bbf08b0a3366f55cfc6ad4681a12ab0946534e7b6f90ea8992d530ec3daa6b523b3cf03101c60cadd914f30dec932c1ef4341b5a8efac3c921e203574cfe0f1f83433fddb8ccfd273f7c3cab7bc27efe3bb61fdccd5146f1185364b9b621e7fb2b74b51f5ee6be72ab6ff46a6359dc2c855e61469724c1dbeb273df9d2e1c1fb74891239c0019dc12d5c7535f7238f963b761d7102b585372cf021b64c4fc85bfb3161e59d2e298bba44cfd34d6859d9dba9dc6271e5047d525468c814f2ae438474b0a977273036da1a2292f88fcfb89574a6bdca1185b40f8aa54026d5926725f99ef028da1be892e3586361efe15f4a148ff1bc9") + val attribution2 = FailurePacket.Attribution.create(Some(attribution1), error1, 3 milliseconds, sharedSecret2) + assert(attribution2 == hex"34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6dc641f68e356323d766fff0aaca5039fe7fc27038195844951a97d5a5b26698a4ca1e9cd4bca1fcca0aac5fee91b18977d2ad0e399ba159733fc98f6e96898ebc39bf0028c9c81619233bab6fad0328aa183a635fac20437fa6e00e899b2527c3697a8ab7342e42d55a679b176ab76671fcd480a9894cb897fa6af0a45b917a162bed6c491972403185df7235502f7ada65769d1bfb12d29f10e25b0d3cc08bbf6de8481ac5c04df32b4533b4f764c2aefb7333202645a629fb16e4a208e9045dc36830759c852b31dd613d8b2b10bbead1ed4eb60c85e8a4517deba5ab53e39867c83c26802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a") val error3 = FailurePacket.wrap(error2, sharedSecret1) assert(error3 == hex"1b4b09a935ce7af95b336baae307f2b400e3a7e808d9b4cf421cc4b3955620acb69dcdb656128dae8857adbd4e6b37fbb1be9c1f2f02e61e9e59a630c4c77cf383cb37b07413aa4de2f2fbf5b40ae40a91a8f4c6d74aeacef1bb1be4ecbc26ec2c824d2bc45db4b9098e732a769788f1cff3f5b41b0d25c132d40dc5ad045ef0043b15332ca3c5a09de2cdb17455a0f82a8f20da08346282823dab062cdbd2111e238528141d69de13de6d83994fbc711e3e269df63a12d3a4177c5c149150eb4dc2f589cd8acabcddba14dec3b0dada12d663b36176cd3c257c5460bab93981ad99f58660efa9b31d7e63b39915329695b3fa60e0a3bdb93e7e29a54ca6a8f360d3848866198f9c3da3ba958e7730847fe1e6478ce8597848d3412b4ae48b06e05ba9a104e648f6eaf183226b5f63ed2e68f77f7e38711b393766a6fab7921b03eba82b5d7cb78e34dc961948d6161eadd7cf5d95d9c56df2ff5faa6ccf85eacdc9ff2fc3abafe41c365a5bd14fd486d6b5e2f24199319e7813e02e798877ffe31a70ae2398d9e31b9e3727e6c1a3c0d995c67d37bb6e72e9660aaaa9232670f382add2edd468927e3303b6142672546997fe105583e7c5a3c4c2b599731308b5416e6c9a3f3ba55b181ad0439d3535356108b059f2cb8742eed7a58d4eba9fe79eaa77c34b12aff1abdaea93197aabd0e74cb271269ca464b3b06aef1d6573df5e1224179616036b368677f26479376681b772d3760e871d99efd34cca5cd6beca95190d967da820b21e5bec60082ea46d776b0517488c84f26d12873912d1f68fafd67bcf4c298e43cfa754959780682a2db0f75f95f0598c0d04fd014c50e4beb86a9e37d95f2bba7e5065ae052dc306555bca203d104c44a538b438c9762de299e1c4ad30d5b4a6460a76484661fc907682af202cd69b9a4473813b2fdc1142f1403a49b7e69a650b7cde9ff133997dcc6d43f049ecac5fce097a21e2bce49c810346426585e3a5a18569b4cddd5ff6bdec66d0b69fcbc5ab3b137b34cc8aefb8b850a764df0e685c81c326611d901c392a519866e132bbb73234f6a358ba284fbafb21aa3605cacbaf9d0c901390a98b7a7dac9d4f0b405f7291c88b2ff45874241c90ac6c5fc895a440453c344d3a365cb929f9c91b9e39cb98b142444aae03a6ae8284c77eb04b0a163813d4c21883df3c0f398f47bf127b5525f222107a2d8fe55289f0cfd3f4bbad6c5387b0594ef8a966afc9e804ccaf75fe39f35c6446f7ee076d433f2f8a44dba1515acc78e589fa8c71b0a006fe14feebd51d0e0aa4e51110d16759eee86192eee90b34432130f387e0ccd2ee71023f1f641cddb571c690107e08f592039fe36d81336a421e89378f351e633932a2f5f697d25b620ffb8e84bb6478e9bd229bf3b164b48d754ae97bd23f319e3c56b3bcdaaeb3bd7fc02ec02066b324cb72a09b6b43dec1097f49d69d3c138ce6f1a6402898baf7568c") + val attribution3 = FailurePacket.Attribution.create(Some(attribution2), error2, 4 milliseconds, sharedSecret1) + assert(attribution3 == hex"74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad54fef30646ae53ccf0855b61f291a81e0ec6dc0f6bf69f0ca0e5889b7e23f577ba67d2a7d6a2aa91264ab9b20630ed52f8ed56cc10a869807cd1a4c2cd802d8433fee5685d6a04edb0bff248a480b93b01904bed3bb31705d1ecb7332004290cc0cd9cc2f7907cf9db28eec02985301668f53fbc28c3e095c8f3a6cd8cab28e5e442fd9ba608b8b12e098731bbfda755393bd403c62289093b40390b2bae337fc87d2606ca028311d73a9ffbdffef56020c735ada30f54e577c6a9ec515ae2739290609503404b118d7494499ecf0457d75015bb60a16288a4959d74cf5ac5d8d6c113de39f748a418d2a7083b90c9c0a09a49149fd1f2d2cde4412e5aa2421eca6fd4f6fe6b2c362ff37d1a0608c931c7ca3b8fefcfd4c44ef9c38357a0767b14f83cb49bd1989fb3f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1") val error4 = FailurePacket.wrap(error3, sharedSecret0) assert(error4 == hex"2dd2f49c1f5af0fcad371d96e8cddbdcd5096dc309c1d4e110f955926506b3c03b44c192896f45610741c85ed4074212537e0c118d472ff3a559ae244acd9d783c65977765c5d4e00b723d00f12475aafaafff7b31c1be5a589e6e25f8da2959107206dd42bbcb43438129ce6cce2b6b4ae63edc76b876136ca5ea6cd1c6a04ca86eca143d15e53ccdc9e23953e49dc2f87bb11e5238cd6536e57387225b8fff3bf5f3e686fd08458ffe0211b87d64770db9353500af9b122828a006da754cf979738b4374e146ea79dd93656170b89c98c5f2299d6e9c0410c826c721950c780486cd6d5b7130380d7eaff994a8503a8fef3270ce94889fe996da66ed121741987010f785494415ca991b2e8b39ef2df6bde98efd2aec7d251b2772485194c8368451ad49c2354f9d30d95367bde316fec6cbdddc7dc0d25e99d3075e13d3de0822669861dafcd29de74eac48b64411987285491f98d78584d0c2a163b7221ea796f9e8671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7cd13a7fe3f72a409bc3bdabfe0ba47a6d861e21f64d2fac706dab18b3e546df4") + val attribution4 = FailurePacket.Attribution.create(Some(attribution3), error3, 5 milliseconds, sharedSecret0) + assert(attribution4 == hex"84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706f42be9999a62ed49c7a81fc73c0b4a16419fd6d334532f40bf179dd19afec21bd8519d5e6ebc3802501ef373bc378eee1f14a6fc5fab5b697c91ce31d5922199d1b0ad5ee12176aacafc7c81d54bc5b8fb7e63f3bfd40a3b6e21f985340cbd1c124c7f85f0369d1aa86ebc66def417107a7861131c8bcd73e8946f4fb54bfac87a2dc15bd7af642f32ae583646141e8875ef81ec9083d7e32d5f135131eab7a43803360434100ff67087762bbe3d6afe2034f5746b8c50e0c3c20dd62a4c174c38b1df7365dccebc7f24f19406649fbf48981448abe5c858bbd4bef6eb983ae7a23e9309fb33b5e7c0522554e88ca04b1d65fc190947dead8c0ccd32932976537d869b5ca53ed4945bccafab2a014ea4cbdc6b0250b25be66ba0afff2ff19c0058c68344fd1b9c472567147525b13b1bc27563e61310110935cf89fda0e34d0575e2389d57bdf2869398ca2965f64a6f04e1d1c2edf2082b97054264a47824dd1a9691c27902b39d57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad") // origin parses error packet and can see that it comes from node #4 - val Right(DecryptedFailurePacket(pubkey, parsedFailure)) = FailurePacket.decrypt(error4, sharedSecrets) + val HtlcFailure(holdTimes, Right(DecryptedFailurePacket(pubkey, parsedFailure))) = FailurePacket.decrypt(error4, Some(attribution4), sharedSecrets) + assert(holdTimes == Seq(HoldTime(5 millisecond, publicKeys(0)), HoldTime(4 milliseconds, publicKeys(1)), HoldTime(3 milliseconds, publicKeys(2)), HoldTime(2 milliseconds, publicKeys(3)), HoldTime(1 milliseconds, publicKeys(4)))) assert(pubkey == publicKeys(4)) assert(parsedFailure == failure) } } + test("only some nodes in the route support attributable failures") { + for ((payloads, packetPayloadLength) <- Seq((referencePaymentPayloads, 1300), (paymentPayloadsFull, 1300))) { + // origin build the onion packet + val Success(PacketAndSecrets(packet, sharedSecrets)) = create(sessionKey, packetPayloadLength, publicKeys, payloads, associatedData) + // each node parses and forwards the packet + val Right(DecryptedPacket(_, packet1, sharedSecret0)) = peel(privKeys(0), associatedData, packet) + val Right(DecryptedPacket(_, packet2, sharedSecret1)) = peel(privKeys(1), associatedData, packet1) + val Right(DecryptedPacket(_, packet3, sharedSecret2)) = peel(privKeys(2), associatedData, packet2) + val Right(DecryptedPacket(_, packet4, sharedSecret3)) = peel(privKeys(3), associatedData, packet3) + val Right(lastPacket@DecryptedPacket(_, _, sharedSecret4)) = peel(privKeys(4), associatedData, packet4) + assert(lastPacket.isLastPacket) + + // node #4 want to reply with an error message + val failure = IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(800000), TlvStream(Set.empty[FailureMessageTlv])) + val failurePacket = createCustomLengthFailurePacket(failure, sharedSecret4, 1024) + val error = Sphinx.FailurePacket.wrap(failurePacket, sharedSecret4) + val error1 = FailurePacket.wrap(error, sharedSecret3) + // node #4 does not support attributable failures, nodes #0 to #3 support attributable failures + val attribution1 = FailurePacket.Attribution.create(None, error, 2 milliseconds, sharedSecret3) + val error2 = FailurePacket.wrap(error1, sharedSecret2) + val attribution2 = FailurePacket.Attribution.create(Some(attribution1), error1, 3 milliseconds, sharedSecret2) + val error3 = FailurePacket.wrap(error2, sharedSecret1) + val attribution3 = FailurePacket.Attribution.create(Some(attribution2), error2, 4 milliseconds, sharedSecret1) + val error4 = FailurePacket.wrap(error3, sharedSecret0) + val attribution4 = FailurePacket.Attribution.create(Some(attribution3), error3, 5 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) + // We're missing attribution data from node #4 but we get hold times until node #3 + assert(holdTimes == Seq(HoldTime(5 millisecond, publicKeys(0)), HoldTime(4 milliseconds, publicKeys(1)), HoldTime(3 milliseconds, publicKeys(2)), HoldTime(2 milliseconds, publicKeys(3)))) + assert(pubkey == publicKeys(4)) + assert(parsedFailure == failure) + } + } + + test("failing node tries to hide its identity") { + for ((payloads, packetPayloadLength) <- Seq((referencePaymentPayloads, 1300), (paymentPayloadsFull, 1300))) { + // origin build the onion packet + val Success(PacketAndSecrets(packet, sharedSecrets)) = create(sessionKey, packetPayloadLength, publicKeys, payloads, associatedData) + // each node parses and forwards the packet + val Right(DecryptedPacket(_, packet1, sharedSecret0)) = peel(privKeys(0), associatedData, packet) + val Right(DecryptedPacket(_, packet2, sharedSecret1)) = peel(privKeys(1), associatedData, packet1) + val Right(DecryptedPacket(_, packet3, sharedSecret2)) = peel(privKeys(2), associatedData, packet2) + val Right(DecryptedPacket(_, packet4, sharedSecret3)) = peel(privKeys(3), associatedData, packet3) + val Right(lastPacket@DecryptedPacket(_, _, sharedSecret4)) = peel(privKeys(4), associatedData, packet4) + assert(lastPacket.isLastPacket) + + // node #4 fails but instead of returning a failure message it returns random data + val error = Sphinx.FailurePacket.wrap(randomBytes(1024), sharedSecret4) + val error1 = FailurePacket.wrap(error, sharedSecret3) + val attribution1 = FailurePacket.Attribution.create(None, error, 2 milliseconds, sharedSecret3) + val error2 = FailurePacket.wrap(error1, sharedSecret2) + val attribution2 = FailurePacket.Attribution.create(Some(attribution1), error1, 3 milliseconds, sharedSecret2) + val error3 = FailurePacket.wrap(error2, sharedSecret1) + val attribution3 = FailurePacket.Attribution.create(Some(attribution2), error2, 4 milliseconds, sharedSecret1) + val error4 = FailurePacket.wrap(error3, sharedSecret0) + val attribution4 = FailurePacket.Attribution.create(Some(attribution3), error3, 5 milliseconds, sharedSecret0) + + // origin can't parse the failure packet but the hold times tell us that nodes #0 to #2 are honest + val HtlcFailure(holdTimes, Left(CannotDecryptFailurePacket(_))) = FailurePacket.decrypt(error4, Some(attribution4), sharedSecrets) + assert(holdTimes == Seq(HoldTime(5 millisecond, publicKeys(0)), HoldTime(4 milliseconds, publicKeys(1)), HoldTime(3 milliseconds, publicKeys(2)), HoldTime(2 milliseconds, publicKeys(3)))) + } + } + test("last node replies with a failure message (arbitrary length)") { val Success(PacketAndSecrets(packet, sharedSecrets)) = create(sessionKey, 1300, publicKeys, referencePaymentPayloads, associatedData) val Right(DecryptedPacket(_, packet1, sharedSecret0)) = peel(privKeys(0), associatedData, packet) @@ -334,7 +412,7 @@ class SphinxSpec extends AnyFunSuite { assert(lastPacket.isLastPacket) // node #4 want to reply with an error message using a custom length - val error = createCustomLengthFailurePacket(TemporaryNodeFailure(), sharedSecret4, 1024) + val error = Sphinx.FailurePacket.wrap(createCustomLengthFailurePacket(TemporaryNodeFailure(), sharedSecret4, 1024), sharedSecret4) assert(error == hex"4ca0784803691f89f7558ff4560ba55aa6b94486e5c5cf1d0922750ad01e185ba8cb9168b60f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4f5984bc119af09d471a61f39e9e389c4120cadabc5d9b7b1355a8ccef050ca8ad72f642fc26919927b347808bade4b1c321b08bc363f20745ba2f97f0ced2996a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fcb4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0ecacb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b8285727735ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb2fe0329666477ee7cb7ebd6b88ad5add8b217188b1ca0fa13de1ec09cc674346875105be6e0e0d6c8928eb0df23c39a639e04e4aedf535c4e093f08b2c905a14f25c0c0fe47a5a1535ab9eae0d9d67bdd79de13a08d59ee05385c7ea4af1ad3248e61dd22f8990e9e99897d653dd7b1b1433a6d464ea9f74e377f2d8ce99ba7dbc753297644234d25ecb5bd528e2e2082824681299ac30c05354baaa9c3967d86d7c07736f87fc0f63e5036d47235d7ae12178ced3ae36ee5919c093a02579e4fc9edad2c446c656c790704bfc8e2c491a42500aa1d75c8d4921ce29b753f883e17c79b09ea324f1f32ddf1f3284cd70e847b09d90f6718c42e5c94484cc9cbb0df659d255630a3f5a27e7d5dd14fa6b974d1719aa98f01a20fb4b7b1c77b42d57fab3c724339d459ee4a1c6b5d3bd4e08624c786a257872acc9ad3ff62222f2265a658d9f2a007229a5293b67ec91c84c4b4407c228434bad8a815ca9b256c776bd2c9f") val error1 = FailurePacket.wrap(error, sharedSecret3) assert(error1 == hex"2ddcd9ac6122dc79b8b96c5e0c20c40bb64d656a8785420bcdacd3cb67dd27bca081bab1626a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca2700c1b46d3f10242ceb286acec56576cf0e22042426c5a61d80c0298dc5ce158f46e11eaf8f32cd44d5f1213d4738768f081978420697b454700ade1c093c02a6ca0e78a7e2f3d9e5c7e49e20c3a56b624bfea51196ec9e88e4e56be38ff56031369f45f1e03be826d44a182f270c153ee0d9f8cf9f1f4132f33974e37c7887d5b857365c873cb218cbf20d4be3abdb2a2011b14add0a5672e01e5845421cf6dd6faca1f2f443757aae575c53ab797c2227ecdab03882bbbf4599318cefafa72fa0c9a0f5a51d13c9d0e5d25bfcfb0154ed25895260a9df8743ac188714a3f16960e6e2ff663c08bffda41743d50960ea2f28cda0bc3bd4a180e297b5b41c700b674cb31d99c7f2a1445e121e772984abff2bbe3f42d757ceeda3d03fb1ffe710aecabda21d738b1f4620e757e57b123dbc3c4aa5d9617dfa72f4a12d788ca596af14bea583f502f16fdc13a5e739afb0715424af2767049f6b9aa107f69c5da0e85f6d8c5e46507e14616d5d0b797c3dea8b74a1b12d4e47ba7f57f09d515f6c7314543f78b5e85329d50c5f96ee2f55bbe0df742b4003b24ccbd4598a64413ee4807dc7f2a9c0b92424e4ae1b418a3cdf02ea4da5c3b12139348aa7022cc8272a3a1714ee3e4ae111cffd1bdfd62c503c80bdf27b2feaea0d5ab8fe00f9cec66e570b00fd24b4a2ed9a5f6384f148a4d6325110a41ca5659ebc5b98721d298a52819b6fb150f273383f1c5754d320be428941922da790e17f482989c365c078f7f3ae100965e1b38c052041165295157e1a7c5b7a57671b842d4d85a7d971323ad1f45e17a16c4656d889fc75c12fc3d8033f598306196e29571e414281c5da19c12605f48347ad5b4648e371757cbe1c40adb93052af1d6110cfbf611af5c8fc682b7e2ade3bfca8b5c7717d19fc9f97964ba6025aebbc91a6671e259949dcf40984342118de1f6b514a7786bd4f6598ffbe1604cef476b2a4cb1343db608aca09d1d38fc23e98ee9c65e7f6023a8d1e61fd4f34f753454bd8e858c8ad6be6403edc599c220e03ca917db765980ac781e758179cd93983e9c1e769e4241d47c") @@ -346,7 +424,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, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, None, sharedSecrets).failure assert(pubkey == publicKeys(4)) assert(failure == TemporaryNodeFailure()) } @@ -361,12 +439,12 @@ class SphinxSpec extends AnyFunSuite { val Right(DecryptedPacket(_, _, sharedSecret2)) = peel(privKeys(2), associatedData, packet2) // node #2 want to reply with an error message - val error = FailurePacket.create(sharedSecret2, InvalidRealm()) + val error = createAndWrap(sharedSecret2, InvalidRealm()) val error1 = FailurePacket.wrap(error, sharedSecret1) 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, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, None, sharedSecrets).failure assert(pubkey == publicKeys(2)) assert(failure == InvalidRealm()) } @@ -379,7 +457,7 @@ class SphinxSpec extends AnyFunSuite { val Right(DecryptedPacket(_, _, sharedSecret2)) = peel(privKeys(2), associatedData, packet2) // node #2 want to reply with an error message - val error = createCustomLengthFailurePacket(InvalidRealm(), sharedSecret2, 1024) + val error = Sphinx.FailurePacket.wrap(createCustomLengthFailurePacket(InvalidRealm(), sharedSecret2, 1024), sharedSecret2) assert(error == hex"f1ca7d3b281a71af53d4a0f83f22b618aae9f9c11b1f3302b13615c66d9aefcc5f1938ef23b9dfa61e3d576b149bedaf83058f85f06a3172a3223ad6c4732d96b32955da7d2feb4140e58d86fc0f2eb5d9d1878e6f8a7f65ab9212030e8e915573ebbd7f35e1a430890be7e67c3fb4bbf2def662fa625421e7b411c29ebe81ec67b77355596b05cc155755664e59c16e21410aabe53e80404a615f44ebb31b365ca77a6e91241667b26c6cad24fb2324cf64e8b9dd6e2ce65f1f098cfd1ef41ba2d4c7def0ff165a0e7c84e7597c40e3dffe97d417c144545a0e38ee33ebaae12cc0c14650e453d46bfc48c0514f354773435ee89b7b2810606eb73262c77a1d67f3633705178d79a1078c3a01b5fadc9651feb63603d19decd3a00c1f69af2dab2595931ca50d8280758b1cc91ba2dc43dbbc3d91bf25c08b46c2ecef7a32cec64d4b61ee3a629ef563afe058b71e71bcb69033948bc8728c5ebe65ec596e4f305b9fc159d53f723dfc95b57f3d51717f1c89af97a6d587e89e62efcc92198a1b2bd66e2d875505ea4046c04389f8cb0ee98f0af03af2652e2f3d9a9c48430f2891a4d9b16e7d18099e4a3dd334c24aba1e2450792c2f22092c170da549d43a440021e699bd6c20d8bbf1961100a01ebcce06a4609f5ad93066287acf68294cfa9ea7cea03a508983b134a9f0118b16409a61c06aaa95897d2067cb7cd59123f3e2ccf0e16091571d616c44818f118bb7835a679f5c0eea8cf1bd5479882b2c2a341ec26dbe5da87b3d37d66b1fbd176f71ab203a3b6eaf7f214d579e7d0e4a3e59089ebd26ba04a62403ae7a793516ec16d971d51c5c0107a917d1a70221e6de16edca7cb057c7d06902b5191f298aa4d478a0c3a6260c257eae504ebbf2b591688e6f3f77af770b6f566ae9868d2f26c12574d3bf9323af59f0fe0072ff94ae597c2aa6fbcbf0831989e02f9d3d1b9fd6dd97f509185d9ecbf272e38bd621ee94b97af8e1cd43853a8f6aa6e8372585c71bf88246d064ade524e1e0bd8496b620c4c2d3ae06b6b064c97536aaf8d515046229f72bee8aa398cd0cc21afd5449595016bef4c77cb1e2e9d31fe1ca3ffde06515e6a4331ccc84edf702e5777b10fc844faf17601a4be3235931f6feca4582a8d247c1d6e4773f8fb6de320cf902bbb1767192782dc550d8e266e727a2aa2a414b816d1826ea46af71701537193c22bbcc0123d7ff5a23b0aa8d7967f36fef27b14fe1866ff3ab215eb29e07af49e19174887d71da7e7fe1b7aa1b3c805c063e0fafedf125fa6c57e38cce33a3f7bb35fd8a9f0950de3c22e49743c05f40bc55f960b8a8b5e2fde4bb229f125538438de418cb318d13968532499118cb7dcaaf8b6d635ac4001273bdafd12c8ea0702fb2f0dac81dbaaf68c1c32266382b293fa3951cb952ed5c1bdc41750cdbc0bd62c51bb685616874e251f031a929c06faef5bfcb0857f815ae20620b823f0abecfb5") val error1 = FailurePacket.wrap(error, sharedSecret1) assert(error1 == hex"fedab5542d8cfc76425c1960d1676ac551116628b2859535ed74f8934d38b82c175c570b34788dbfd0048e4a41c2bb01acf21a928c09f96b801d011d5ff805731f476679849797e76d1ace72304509e05adbbcf0f74959d7d370af32fa27066b9a7a9cb91d92518f3bdabe35a8b3ecea116db79b0c011b70742599012741c4128ca6655eeaf7e6ff343fed810af0e069fa1650659d5864f8b9f1aea92f1fcc10b1b71f3b012e1e55e53056d7f5e092daf7eb1b9244d2de468f69730f3237ce39a84cfd0ee42b12e5ac7ca63fea15bee528125e135090988e55fda565f99f15787ab49ae2b536ca34b1732069a72f314c99836091c17f4e50afecc602184c1e656cf6eb752a4ded94df315a3e16e3d3e422517e9b9a5c566f8bf3eb6144a6778df0078b51887d8ea59b73416c59594f81f8bf1b0f1c98b3d9d5ed87fe76358a47df8a705fb3edddf64770c2d49744854a5ed0272d94cec1cd1b049a6dece2e4aade89d783634c259a330bc407af06368aece354d6fe73608716da08a037dec9c71c4c73bd4a6c86fd1820b54aa2602132a95495933a24e28b189219859ab46847340ad08968a70d5a0df8223aab06a6ea532a4cb25498f3687361a59b9896975c948e03ba60f5248a1f2f4d7aa6e8f00f82f6ca92273f6084cac56c51d4dda2511d64d88dcfd11df5a07ae6779d445f141f5759fca37e09826e2e481ed5dced02956104b219f839f508f60d8828250d0a3617b9d021fad48cde24a5cb42e3278dff0d95af795d4c71bccd344fa98129c9d6f53dd4f7acab78a98711fc5d04112ae971dedc97649608597b7e53369be2fe3f9b0e6b349b3fadcf9bd2a3d24b5e876c74e1006f7c330714ea5146986f3f73b09cae5cdf6277e23a34ecce0d92d909442743ef415be81050c341eb305e93b14b07b55c079766cd894ea00826ce50d5c45707870b0cf411113b8e4e43cf34caf79f3936fdfdbeae185ff52db69ca72442d892ed0e45b9fac939aa172bfa873cebee1e2196fe124597feb92880339ebca8233acaf3061591ed8cf290dfd9b0a06d7efc299993b9c680451992e15d2cb8b5b4a3dc1e511a39d781818144a9662bdfbd01371e898c454a8a092b7a0d32a8d58aec8134891a974ac7b297c3b4f94100083db891bde0ebc1e737dc6c33dad87cf20429d1b865c7e8ee5032d66a17c5a731d288dda8fc38e2c963c317f12a786ded3eac484dcc11b5c530dec0e4cc40ec4bb2c529555a51d8655a4de08fdde774781b5672150d1c771bf0916fc5df6ddb2f2e683e86aa23a52c0fc2efe72eeb1fa5f86ad7926685f40d57ab19b29e1ce5dce8c98ae35aacf740cacb257915fe8421ec09d0883d4ae41fe2695679264b0196e8d0b874d47e2fd675c9dfba26e666d407572e19a65c84ca54ac7235ef1bd4aedd9b0f6406cc7eeb08020e325f22396bc1a42d2de5ff71042b4e098cb0358741a50757a31c45de1f7ecf3a5e5e06f8b682f0") @@ -387,7 +465,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, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, None, sharedSecrets).failure assert(pubkey == publicKeys(2)) assert(failure == InvalidRealm()) } @@ -600,7 +678,11 @@ object SphinxSpec { def createCustomLengthFailurePacket(failure: FailureMessage, sharedSecret: ByteVector32, length: Int): ByteVector = { val um = Sphinx.generateKey("um", sharedSecret) val packet = FailureMessageCodecs.failureOnionCodec(Hmac256(um), length).encode(failure).require.toByteVector - Sphinx.FailurePacket.wrap(packet, sharedSecret) + packet + } + + def createAndWrap(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { + FailurePacket.wrap(FailurePacket.create(sharedSecret, failure), sharedSecret) } val privKeys = Seq( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala index f5f395be19..13fd6a4179 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala @@ -19,10 +19,11 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FAIL_MALFORMED_HTLC, CMD_FULFILL_HTLC, HtlcSettlementCommand} +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.pg.PgPendingCommandsDb import fr.acinq.eclair.db.sqlite.SqlitePendingCommandsDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{setVersion, using} -import fr.acinq.eclair.randomBytes32 +import fr.acinq.eclair.{TimestampMilli, randomBytes, randomBytes32} import fr.acinq.eclair.wire.internal.CommandCodecs.cmdCodec import fr.acinq.eclair.wire.protocol.{FailureMessageCodecs, FailureReason, UnknownNextPeer} import org.scalatest.funsuite.AnyFunSuite @@ -53,14 +54,14 @@ class PendingCommandsDbSpec extends AnyFunSuite { val channelId2 = randomBytes32() val msg0 = CMD_FULFILL_HTLC(0, randomBytes32()) val msg1 = CMD_FULFILL_HTLC(1, randomBytes32()) - val msg2 = CMD_FAIL_HTLC(2, FailureReason.EncryptedDownstreamFailure(randomBytes32())) - val msg3 = CMD_FAIL_HTLC(3, FailureReason.EncryptedDownstreamFailure(randomBytes32())) + val msg2 = CMD_FAIL_HTLC(2, FailureReason.EncryptedDownstreamFailure(randomBytes32(), None), None) + val msg3 = CMD_FAIL_HTLC(3, FailureReason.EncryptedDownstreamFailure(randomBytes32(), Some(randomBytes(Sphinx.FailurePacket.Attribution.totalLength))), Some(TimestampMilli.now())) val msg4 = CMD_FAIL_MALFORMED_HTLC(4, randomBytes32(), FailureMessageCodecs.BADONION) assert(db.listSettlementCommands(channelId1).toSet == Set.empty) db.addSettlementCommand(channelId1, msg0) db.addSettlementCommand(channelId1, msg0) // duplicate - db.addSettlementCommand(channelId1, CMD_FAIL_HTLC(msg0.id, FailureReason.EncryptedDownstreamFailure(randomBytes32()))) // conflicting command + db.addSettlementCommand(channelId1, CMD_FAIL_HTLC(msg0.id, FailureReason.EncryptedDownstreamFailure(randomBytes32(), None), None)) // conflicting command db.addSettlementCommand(channelId1, msg1) db.addSettlementCommand(channelId1, msg2) db.addSettlementCommand(channelId1, msg3) @@ -139,7 +140,7 @@ object PendingCommandsDbSpec { val cmds = (0 until Random.nextInt(5)).map { _ => Random.nextInt(2) match { case 0 => CMD_FULFILL_HTLC(Random.nextLong(100_000), randomBytes32()) - case 1 => CMD_FAIL_HTLC(Random.nextLong(100_000), FailureReason.LocalFailure(UnknownNextPeer())) + case 1 => CMD_FAIL_HTLC(Random.nextLong(100_000), FailureReason.LocalFailure(UnknownNextPeer()), None) } } cmds.map(cmd => TestCase(channelId, cmd)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 00b8f6eea3..5b8150986b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -99,7 +99,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike def createBlindedPacket(amount: MilliSatoshi, paymentHash: ByteVector32, expiry: CltvExpiry, finalExpiry: CltvExpiry, pathId: ByteVector): IncomingPaymentPacket.FinalPacket = { val add = UpdateAddHtlc(ByteVector32.One, 0, amount, paymentHash, expiry, TestConstants.emptyOnionPacket, Some(randomKey().publicKey), 1.0, None) val payload = FinalPayload.Blinded(TlvStream(AmountToForward(amount), TotalAmount(amount), OutgoingCltv(finalExpiry), EncryptedRecipientData(hex"deadbeef")), TlvStream(PathId(pathId), PaymentConstraints(CltvExpiry(500_000), 1 msat))) - IncomingPaymentPacket.FinalPacket(add, payload) + IncomingPaymentPacket.FinalPacket(add, payload, TimestampMilli.now()) } test("PaymentHandler should reply with a fulfill/fail, emit a PaymentReceived and add payment in DB") { f => @@ -117,7 +117,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(Crypto.sha256(incoming.get.paymentPreimage) == invoice.paymentHash) val add = UpdateAddHtlc(ByteVector32.One, 1, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -133,7 +133,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt11Invoice] val add = UpdateAddHtlc(ByteVector32.One, 1, 75_000 msat, invoice.paymentHash, defaultExpiry + CltvExpiryDelta(12), TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(70_000 msat, 70_000 msat, defaultExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(70_000 msat, 70_000 msat, defaultExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -151,7 +151,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val add = UpdateAddHtlc(ByteVector32.One, 2, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -198,7 +198,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -331,7 +331,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.isExpired()) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(incoming.invoice.isExpired() && incoming.status == IncomingPaymentStatus.Expired) @@ -346,7 +346,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.isExpired()) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) @@ -361,7 +361,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(!invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -376,7 +376,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val lowCltvExpiry = nodeParams.channelConf.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -390,7 +390,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -404,7 +404,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -418,7 +418,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -433,7 +433,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Invalid payment secret. val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -465,7 +465,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt12Invoice] val add = UpdateAddHtlc(ByteVector32.One, 0, 5000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, randomBytes32(), None))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, randomBytes32(), None), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).isEmpty) @@ -545,14 +545,16 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Partial payment missing additional parts. f.sender.send(handler, ReceiveStandardPayment(f.sender.ref, Some(1000 msat), Left("1 slow coffee"))) val pr1 = f.sender.expectMsgType[Bolt11Invoice] + val receivedAt1 = TimestampMilli.now() val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret, pr1.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret, pr1.paymentMetadata), receivedAt1)) // Partial payment exceeding the invoice amount, but incomplete because it promises to overpay. f.sender.send(handler, ReceiveStandardPayment(f.sender.ref, Some(1500 msat), Left("1 slow latte"))) val pr2 = f.sender.expectMsgType[Bolt11Invoice] + val receivedAt2 = receivedAt1 + 1.millis val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret, pr2.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret, pr2.paymentMetadata), receivedAt2)) awaitCond { f.sender.send(handler, GetPendingPayments) @@ -561,8 +563,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val commands = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil assert(commands.toSet == Set( - Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, FailureReason.LocalFailure(PaymentTimeout()), commit = true)), - Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PaymentTimeout()), commit = true)) + Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, FailureReason.LocalFailure(PaymentTimeout()), Some(receivedAt1), commit = true)), + Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PaymentTimeout()), Some(receivedAt2), commit = true)) )) awaitCond({ f.sender.send(handler, GetPendingPayments) @@ -570,8 +572,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike }) // Extraneous HTLCs should be failed. - f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, 1.0, None)), Some(PaymentTimeout()))) - f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PaymentTimeout()), commit = true))) + val receivedAt3 = receivedAt1 + 2.millis + f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, 1.0, None), receivedAt3), Some(PaymentTimeout()))) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PaymentTimeout()), Some(receivedAt3), commit = true))) // The payment should still be pending in DB. val Some(incomingPayment) = nodeParams.db.payments.getIncomingPayment(pr1.paymentHash) @@ -586,16 +589,19 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike f.sender.send(handler, ReceiveStandardPayment(f.sender.ref, Some(1000 msat), Left("1 fast coffee"), paymentPreimage_opt = Some(preimage))) val invoice = f.sender.expectMsgType[Bolt11Invoice] + val receivedAt1 = TimestampMilli.now() val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), receivedAt1)) // Invalid payment secret -> should be rejected. + val receivedAt2 = receivedAt1 + 1.millis val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata), receivedAt2)) + val receivedAt3 = receivedAt1 + 2.millis val add3 = add2.copy(id = 43) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), receivedAt3)) f.register.expectMsgAllOf( - Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), + Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), Some(receivedAt2), commit = true)), Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, commit = true)), Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, commit = true)) ) @@ -611,7 +617,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike }) // Extraneous HTLCs should be fulfilled. - f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, 1.0, None)), None)) + f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, 1.0, None), TimestampMilli.now()), None)) f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, commit = true))) assert(f.eventListener.expectMsgType[PaymentReceived].amount == 200.msat) val received2 = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) @@ -630,9 +636,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = f.sender.expectMsgType[Bolt11Invoice] val add1 = UpdateAddHtlc(randomBytes32(), 0, 1100 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1500 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1500 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val add2 = UpdateAddHtlc(randomBytes32(), 1, 500 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1500 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1500 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) f.register.expectMsgAllOf( Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, commit = true)), @@ -657,17 +663,18 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.paymentHash == Crypto.sha256(preimage)) val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) - f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, FailureReason.LocalFailure(PaymentTimeout()), commit = true))) + val receivedAt1 = TimestampMilli.now() + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), receivedAt1)) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, FailureReason.LocalFailure(PaymentTimeout()), Some(receivedAt1), commit = true))) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty }) val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata), TimestampMilli.now())) // the fulfill are not necessarily in the same order as the commands f.register.expectMsgAllOf( @@ -699,7 +706,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload)) + sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload, TimestampMilli.now())) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -720,7 +727,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload)) + sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload, TimestampMilli.now())) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -742,9 +749,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, payload)) + val receivedAt = TimestampMilli.now() + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, payload, receivedAt)) - f.register.expectMsg(Register.Forward(null, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), commit = true))) + f.register.expectMsg(Register.Forward(null, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), Some(receivedAt), commit = true))) assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) } @@ -756,7 +764,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, paymentSecret, None))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, paymentSecret, None), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) @@ -770,7 +778,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345")))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345")), TimestampMilli.now())) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) @@ -786,7 +794,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) val invoice = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, None, paymentHash, randomKey(), Left("dummy"), CltvExpiryDelta(12)) val incomingPayment = IncomingStandardPayment(invoice, paymentPreimage, PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) - val fulfill = DoFulfill(incomingPayment, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add)))) + val fulfill = DoFulfill(incomingPayment, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add, TimestampMilli.now())))) sender.send(handlerWithoutMpp, fulfill) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala index dcdd2ef225..6d6f70f720 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM._ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilli, randomBytes32} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector @@ -233,7 +233,7 @@ object MultiPartPaymentFSMSpec { def createMultiPartHtlc(totalAmount: MilliSatoshi, htlcAmount: MilliSatoshi, htlcId: Long): HtlcPart = { val htlc = UpdateAddHtlc(htlcIdToChannelId(htlcId), htlcId, htlcAmount, paymentHash, CltvExpiry(42), TestConstants.emptyOnionPacket, None, 1.0, None) - HtlcPart(totalAmount, htlc) + HtlcPart(totalAmount, htlc, TimestampMilli.now()) } } \ No newline at end of file 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 4d791ac85b..23bbcb27e5 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 @@ -408,7 +408,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)))))), - UnreadableRemoteFailure(finalAmount, Nil, randomBytes(292)) + UnreadableRemoteFailure(finalAmount, Nil, randomBytes(292), Nil) ) val extraEdges1 = Seq( ExtraEdge(a, b, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12), 1 msat, None), @@ -444,14 +444,14 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId1, failedRoute1) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, randomBytes(292))))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, randomBytes(292), Nil)))) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(500_000 msat, hop_ad :: hop_de :: Nil, None)))) childPayFsm.expectMsgType[SendPaymentToRoute] assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(failedId1)) val (failedId2, failedRoute2) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head - val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, failedRoute2.hops, randomBytes(292))))) + val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, failedRoute2.hops, randomBytes(292), Nil)))) assert(result.failures.length >= 3) assert(result.failures.contains(LocalFailure(finalAmount, Nil, RetryExhausted))) @@ -539,7 +539,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, randomBytes(292))))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, randomBytes(292), Nil)))) router.expectMsgType[RouteRequest] val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout()))))) @@ -557,7 +557,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(UnreadableRemoteFailure(failedRoute.amount, failedRoute.fullRoute, randomBytes(292))))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.amount, failedRoute.fullRoute, randomBytes(292), Nil)))) router.expectMsgType[RouteRequest] val result = fulfillPendingPayments(f, 1, e, finalAmount) 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 b7d1c4ad20..13db8bccb7 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 @@ -28,7 +28,8 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateReque import fr.acinq.eclair.channel.Register.{ForwardShortId, ForwardShortIdFailure} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.Sphinx.HoldTime +import fr.acinq.eclair.crypto.{Sphinx, SphinxSpec} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.Invoice.ExtraEdge @@ -512,7 +513,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) val failure = TemporaryChannelFailure(Some(update_bc)) - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) // payment lifecycle will ask the router to temporarily exclude this channel from its route calculations assert(routerForwarder.expectMsgType[ChannelCouldNotRelay].hop.shortChannelId == update_bc.shortChannelId) @@ -545,7 +546,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat) val failure = IncorrectCltvExpiry(CltvExpiry(5), Some(channelUpdate_bc_modified)) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING expectRouteRequest(routerForwarder, a, cfg) @@ -560,7 +561,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(43), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat) val failure2 = IncorrectCltvExpiry(CltvExpiry(5), Some(channelUpdate_bc_modified_2)) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets2.head.secret, failure2))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets2.head.secret, failure2))))) // this time the payment lifecycle will ask the router to temporarily exclude this channel from its route calculations routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c), Some(nodeParams.routerConf.channelExcludeDuration))) @@ -590,7 +591,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // the node replies with a temporary failure containing the same update as the one we already have (likely a balance issue) val failure = TemporaryChannelFailure(Some(update_bc)) - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) // we should temporarily exclude that channel assert(routerForwarder.expectMsgType[ChannelCouldNotRelay].hop.shortChannelId == update_bc.shortChannelId) routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c), Some(nodeParams.routerConf.channelExcludeDuration))) @@ -624,7 +625,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat) val failure = IncorrectCltvExpiry(CltvExpiry(5), Some(channelUpdate_bc_modified)) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING val extraEdges1 = Seq( @@ -663,7 +664,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we disable the channel val channelUpdate_cd_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(42), update_cd.htlcMinimumMsat, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.htlcMaximumMsat, enable = false) val failure = ChannelDisabled(channelUpdate_cd_disabled.messageFlags, channelUpdate_cd_disabled.channelFlags, Some(channelUpdate_cd_disabled)) - val failureOnion = Sphinx.FailurePacket.wrap(Sphinx.FailurePacket.create(sharedSecrets1(1).secret, failure), sharedSecrets1.head.secret) + val failureOnion = Sphinx.FailurePacket.wrap(SphinxSpec.createAndWrap(sharedSecrets1(1).secret, failure), sharedSecrets1.head.secret) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, failureOnion)))) assert(routerForwarder.expectMsgType[RouteCouldRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, update_bc).map(_.shortChannelId)) @@ -686,7 +687,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) // payment lifecycle forwards the embedded channelUpdate to the router awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) @@ -725,7 +726,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // The payment fails inside the blinded route: the introduction node sends back an error. val failure = InvalidOnionBlinding(randomBytes32()) - val failureOnion = Sphinx.FailurePacket.create(sharedSecrets.head.secret, failure) + val failureOnion = SphinxSpec.createAndWrap(sharedSecrets.head.secret, failure) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, failureOnion)))) // We retry but we exclude the failed blinded route. @@ -900,12 +901,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { (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))), - // unreadable remote failures -> blacklist all nodes except our direct peer, the final recipient or the last hop - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ByteVector.empty), Set.empty, Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, ByteVector.empty), Set(c), Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: channelHopFromUpdate(d, e, update_de) :: Nil, ByteVector.empty), Set(c, d), Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: NodeHop(d, e, CltvExpiryDelta(24), 0 msat) :: Nil, ByteVector.empty), Set(c), Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: blindedHop_de :: Nil, ByteVector.empty), Set(c), Set.empty), + // 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, ByteVector.empty, Nil), Set.empty, Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, ByteVector.empty, Nil), Set(c), Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: channelHopFromUpdate(d, e, update_de) :: Nil, ByteVector.empty, Nil), Set(c, d), Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: channelHopFromUpdate(d, e, update_de) :: Nil, ByteVector.empty, Seq(HoldTime(100 millis, b), HoldTime(90 millis, c), HoldTime(80 millis, d))), Set(d), Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: NodeHop(d, e, CltvExpiryDelta(24), 0 msat) :: Nil, ByteVector.empty, Nil), Set(c), Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: blindedHop_de :: Nil, ByteVector.empty, Nil), Set(c), Set.empty), ) for ((failure, expectedNodes, expectedChannels) <- testCases) { @@ -967,7 +969,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat) val failure = IncorrectCltvExpiry(CltvExpiry(5), Some(channelUpdate_bc_modified)) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head.secret, failure))))) + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure))))) // The payment fails without retrying sender.expectMsgType[PaymentFailed] 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 62da32a8de..a8d63586bb 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 @@ -25,6 +25,7 @@ import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.crypto.Sphinx.HoldTime import fr.acinq.eclair.crypto.{ShaChain, Sphinx} import fr.acinq.eclair.payment.IncomingPaymentPacket._ import fr.acinq.eclair.payment.OutgoingPaymentPacket._ @@ -79,7 +80,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { def testPeelOnion(packet_b: OnionRoutingPacket): Unit = { val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, packet_b, None, 1.0, None) - val Right(relay_b@ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val Right(relay_b@ChannelRelayPacket(add_b2, payload_b, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(add_b2 == add_b) assert(packet_c.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_b.amountToForward == amount_bc) @@ -89,7 +90,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(relay_b.expiryDelta == channelUpdate_bc.cltvExpiryDelta) val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, 1.0, None) - val Right(relay_c@ChannelRelayPacket(add_c2, payload_c, packet_d)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val Right(relay_c@ChannelRelayPacket(add_c2, payload_c, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) assert(add_c2 == add_c) assert(packet_d.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_c.amountToForward == amount_cd) @@ -99,7 +100,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(relay_c.expiryDelta == channelUpdate_cd.cltvExpiryDelta) val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, 1.0, None) - val Right(relay_d@ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val Right(relay_d@ChannelRelayPacket(add_d2, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) assert(add_d2 == add_d) assert(packet_e.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_d.amountToForward == amount_de) @@ -109,7 +110,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(relay_d.expiryDelta == channelUpdate_de.cltvExpiryDelta) val add_e = UpdateAddHtlc(randomBytes32(), 2, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) - val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey, Features.empty) + val Right(FinalPacket(add_e2, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(add_e2 == add_e) assert(payload_e.isInstanceOf[FinalPayload.Standard]) assert(payload_e.amount == finalAmount) @@ -133,7 +134,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // let's peel the onion val add_b = UpdateAddHtlc(randomBytes32(), 0, finalAmount, paymentHash, finalExpiry, payment.cmd.onion, None, 1.0, None) - val Right(FinalPacket(add_b2, payload_b)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val Right(FinalPacket(add_b2, payload_b, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(add_b2 == add_b) assert(payload_b.isInstanceOf[FinalPayload.Standard]) assert(payload_b.amount == finalAmount) @@ -150,7 +151,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // let's peel the onion val add_b = UpdateAddHtlc(randomBytes32(), 0, finalAmount + 100.msat, paymentHash, finalExpiry + CltvExpiryDelta(6), payment.cmd.onion, None, 1.0, None) - val Right(FinalPacket(_, payload_b)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val Right(FinalPacket(_, payload_b, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(payload_b.isInstanceOf[FinalPayload.Standard]) assert(payload_b.amount == finalAmount) assert(payload_b.totalAmount == finalAmount) @@ -170,7 +171,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payment.cmd.nextPathKey_opt.isEmpty) val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(relay_b@ChannelRelayPacket(_, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val Right(relay_b@ChannelRelayPacket(_, payload_b, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(packet_c.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_b.amountToForward >= amount_bc) assert(relay_b.outgoingCltv == expiry_bc) @@ -180,7 +181,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payload_b.isInstanceOf[IntermediatePayload.ChannelRelay.Standard]) val add_c = UpdateAddHtlc(randomBytes32(), 1, relay_b.amountToForward, relay_b.add.paymentHash, relay_b.outgoingCltv, packet_c, None, 1.0, None) - val Right(relay_c@ChannelRelayPacket(_, payload_c, packet_d)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) + val Right(relay_c@ChannelRelayPacket(_, payload_c, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) assert(packet_d.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_c.amountToForward >= amount_cd) assert(relay_c.outgoingCltv == expiry_cd) @@ -191,7 +192,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val pathKey_d = payload_c.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey val add_d = UpdateAddHtlc(randomBytes32(), 2, relay_c.amountToForward, relay_c.add.paymentHash, relay_c.outgoingCltv, packet_d, Some(pathKey_d), 1.0, None) - val Right(relay_d@ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) + val Right(relay_d@ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) assert(packet_e.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_d.amountToForward >= amount_de) assert(relay_d.outgoingCltv == expiry_de) @@ -202,7 +203,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val pathKey_e = payload_d.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey val add_e = UpdateAddHtlc(randomBytes32(), 2, relay_d.amountToForward, relay_d.add.paymentHash, relay_d.outgoingCltv, packet_e, Some(pathKey_e), 1.0, None) - val Right(FinalPacket(_, payload_e)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional)) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional)) assert(payload_e.amount == finalAmount) assert(payload_e.totalAmount == finalAmount) assert(add_e.cltvExpiry == finalExpiry) @@ -233,7 +234,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payment.cmd.nextPathKey_opt.isEmpty) val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(relay_b@ChannelRelayPacket(_, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val Right(relay_b@ChannelRelayPacket(_, payload_b, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(packet_c.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) assert(relay_b.amountToForward >= amount_bc) assert(relay_b.outgoingCltv == expiry_bc) @@ -243,7 +244,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payload_b.isInstanceOf[IntermediatePayload.ChannelRelay.Standard]) val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, 1.0, None) - val Right(FinalPacket(_, payload_c)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) + val Right(FinalPacket(_, payload_c, _)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) assert(payload_c.amount == amount_bc) assert(payload_c.totalAmount == amount_bc) assert(payload_c.expiry == expiry_bc) @@ -259,7 +260,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payment.cmd.nextPathKey_opt.nonEmpty) val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(FinalPacket(_, payload_b)) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional)) + val Right(FinalPacket(_, payload_b, _)) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional)) assert(payload_b.amount == finalAmount) assert(payload_b.totalAmount == finalAmount) assert(add_b.cltvExpiry == finalExpiry) @@ -272,14 +273,14 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId) val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount + 100.msat, payment.cmd.paymentHash, payment.cmd.cltvExpiry + CltvExpiryDelta(6), payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(FinalPacket(_, payload_b)) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional)) + val Right(FinalPacket(_, payload_b, _)) = decrypt(add_b, priv_b.privateKey, Features(RouteBlinding -> Optional)) assert(payload_b.amount == finalAmount) assert(payload_b.totalAmount == finalAmount) } private def testRelayTrampolinePayment(invoice: Bolt11Invoice, payment: TrampolinePayment.OutgoingPayment): Unit = { val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, 1.0, None) - val Right(RelayToTrampolinePacket(add_c2, outer_c, inner_c, trampolinePacket_e)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val Right(RelayToTrampolinePacket(add_c2, outer_c, inner_c, trampolinePacket_e, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) assert(add_c2 == add_c) assert(outer_c.amount == payment.trampolineAmount) assert(outer_c.totalAmount == payment.trampolineAmount) @@ -296,12 +297,12 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payment_e.cmd.amount == amount_cd) assert(payment_e.cmd.cltvExpiry == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val Right(ChannelRelayPacket(add_d2, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) assert(add_d2 == add_d) assert(payload_d == IntermediatePayload.ChannelRelay.Standard(channelUpdate_de.shortChannelId, amount_de, expiry_de)) val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) - val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey, Features.empty) + val Right(FinalPacket(add_e2, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(add_e2 == add_e) assert(payload_e.isInstanceOf[FinalPayload.Standard]) assert(payload_e.amount == finalAmount) @@ -345,7 +346,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val payment = TrampolinePayment.buildOutgoingPayment(c, invoice, finalExpiry) val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, 1.0, None) - val Right(RelayToNonTrampolinePacket(_, outer_c, inner_c)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val Right(RelayToNonTrampolinePacket(_, outer_c, inner_c, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) assert(outer_c.amount == payment.trampolineAmount) assert(outer_c.totalAmount == payment.trampolineAmount) assert(outer_c.expiry == payment.trampolineExpiry) @@ -367,12 +368,12 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payment_e.cmd.amount == amount_cd) assert(payment_e.cmd.cltvExpiry == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val Right(ChannelRelayPacket(add_d2, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) assert(add_d2 == add_d) assert(payload_d == IntermediatePayload.ChannelRelay.Standard(channelUpdate_de.shortChannelId, amount_de, expiry_de)) val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) - val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey, Features.empty) + val Right(FinalPacket(add_e2, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(add_e2 == add_e) assert(payload_e.isInstanceOf[FinalPayload.Standard]) assert(payload_e.amount == finalAmount) @@ -407,7 +408,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, 1.0, None) - val Right(RelayToNonTrampolinePacket(_, outer_c, inner_c)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val Right(RelayToNonTrampolinePacket(_, outer_c, inner_c, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) assert(outer_c.amount == payment.trampolineAmount) assert(outer_c.totalAmount == payment.trampolineAmount) assert(outer_c.expiry == payment.trampolineExpiry) @@ -429,12 +430,12 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payment_e.cmd.amount == amount_cd) assert(payment_e.cmd.cltvExpiry == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val Right(ChannelRelayPacket(add_d2, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) assert(add_d2 == add_d) assert(payload_d == IntermediatePayload.ChannelRelay.Standard(channelUpdate_de.shortChannelId, amount_de, expiry_de)) val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) - val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey, Features.empty) + val Right(FinalPacket(add_e2, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(add_e2 == add_e) assert(payload_e.isInstanceOf[FinalPayload.Standard]) assert(payload_e.amount == finalAmount) @@ -473,14 +474,14 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val payment = TrampolinePayment.buildOutgoingPayment(c, invoice, finalExpiry) val add_c = UpdateAddHtlc(randomBytes32(), 2, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, 1.0, None) - val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) // c forwards an invalid trampoline onion to e through d. val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e.copy(payload = trampolinePacket_e.payload.reverse))) val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, 1.0) assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId) val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(_, _, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) val Left(failure) = decrypt(add_e, priv_e.privateKey, Features.empty) @@ -558,7 +559,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // Intermediate nodes can reduce the expiry by at most min_final_expiry_delta. val invalidExpiry = payment.cmd.cltvExpiry - Channel.MIN_CLTV_EXPIRY_DELTA - CltvExpiryDelta(1) val add_d = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, paymentHash, invalidExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(relay_d@ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) + val Right(relay_d@ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) assert(payload_d.outgoing.contains(channelUpdate_de.shortChannelId)) assert(relay_d.outgoingCltv < CltvExpiry(currentBlockCount)) assert(payload_d.isInstanceOf[IntermediatePayload.ChannelRelay.Blinded]) @@ -597,14 +598,14 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to decrypt at the final trampoline node when amount has been decreased by next-to-last trampoline") { val add_c = createIntermediateTrampolinePayment() - val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) // c forwards an invalid amount to e through (the outer total amount doesn't match the inner amount). val invalidTotalAmount = inner_c.amountToForward - 1.msat val recipient_e = ClearRecipient(e, Features.empty, invalidTotalAmount, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e)) val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b)))), paymentHash, Route(invalidTotalAmount, afterTrampolineChannelHops, None), recipient_e, 1.0) val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val Right(ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) val add_e = UpdateAddHtlc(randomBytes32(), 4, payload_d.amountToForward(add_d.amountMsat), paymentHash, payload_d.outgoingCltv(add_d.cltvExpiry), packet_e, None, 1.0, None) val Left(failure) = decrypt(add_e, priv_e.privateKey, Features.empty) @@ -613,14 +614,14 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") { val add_c = createIntermediateTrampolinePayment() - val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val Right(RelayToTrampolinePacket(_, _, inner_c, trampolinePacket_e, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) // c forwards an invalid amount to e through (the outer expiry doesn't match the inner expiry). val invalidExpiry = inner_c.outgoingCltv - CltvExpiryDelta(12) val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, invalidExpiry, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e)) val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L), b)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e, 1.0) val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val Right(ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) val add_e = UpdateAddHtlc(randomBytes32(), 4, payload_d.amountToForward(add_d.amountMsat), paymentHash, payload_d.outgoingCltv(add_d.cltvExpiry), packet_e, None, 1.0, None) val Left(failure) = decrypt(add_e, priv_e.privateKey, Features.empty) @@ -647,26 +648,57 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret) val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, 1.0) val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, 1.0, None) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, 1.0, None) - val Right(ChannelRelayPacket(_, _, packet_d)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val Right(ChannelRelayPacket(_, _, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, 1.0, None) - val Right(ChannelRelayPacket(_, _, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) - val Right(FinalPacket(_, payload_e)) = decrypt(add_e, priv_e.privateKey, Features.empty) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) assert(payload_e.isInstanceOf[FinalPayload.Standard]) // e returns a failure val failure = IncorrectOrUnknownPaymentDetails(finalAmount, BlockHeight(currentBlockCount)) - val Right(fail_e: UpdateFailHtlc) = buildHtlcFailure(priv_e.privateKey, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(failure)), add_e) + val Right(fail_e: UpdateFailHtlc) = buildHtlcFailure(priv_e.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(failure), None), add_e) assert(fail_e.id == add_e.id) - val Right(fail_d: UpdateFailHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, FailureReason.EncryptedDownstreamFailure(fail_e.reason)), add_d) + val Right(fail_d: UpdateFailHtlc) = buildHtlcFailure(priv_d.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_d.id, FailureReason.EncryptedDownstreamFailure(fail_e.reason, None), None), add_d) assert(fail_d.id == add_d.id) - val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, FailureReason.EncryptedDownstreamFailure(fail_d.reason)), add_c) + val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_c.id, FailureReason.EncryptedDownstreamFailure(fail_d.reason, None), None), add_c) assert(fail_c.id == add_c.id) - val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason)), add_b) + 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, payment.sharedSecrets) + val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, fail_b.attribution_opt, payment.sharedSecrets).failure + assert(failingNode == e) + assert(decryptedFailure == failure) + } + + test("build htlc failure onion with attribution data") { + // a -> b -> c -> d -> e + val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret) + val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, 1.0) + val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, 1.0, None) + val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, 1.0, None) + val Right(ChannelRelayPacket(_, _, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty) + val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, 1.0, None) + val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty) + val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, 1.0, None) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty) + assert(payload_e.isInstanceOf[FinalPayload.Standard]) + + // e returns a failure + val failure = IncorrectOrUnknownPaymentDetails(finalAmount, BlockHeight(currentBlockCount)) + val Right(fail_e: UpdateFailHtlc) = buildHtlcFailure(priv_e.privateKey, useAttributableFailures = true, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(failure), Some(TimestampMilli(60))), add_e, now = TimestampMilli(62)) + assert(fail_e.id == add_e.id) + val Right(fail_d: UpdateFailHtlc) = buildHtlcFailure(priv_d.privateKey, useAttributableFailures = true, CMD_FAIL_HTLC(add_d.id, FailureReason.EncryptedDownstreamFailure(fail_e.reason, fail_e.attribution_opt), Some(TimestampMilli(25))), add_d, now = TimestampMilli(63)) + assert(fail_d.id == add_d.id) + val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, useAttributableFailures = true, CMD_FAIL_HTLC(add_c.id, FailureReason.EncryptedDownstreamFailure(fail_d.reason, fail_d.attribution_opt), Some(TimestampMilli(10))), add_c, now = TimestampMilli(70)) + assert(fail_c.id == add_c.id) + val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, useAttributableFailures = true, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason, fail_c.attribution_opt), Some(TimestampMilli(0))), add_b, now = TimestampMilli(76)) + 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(76 milliseconds, b), HoldTime(60 milliseconds, c), HoldTime(38 milliseconds, d), HoldTime(2 milliseconds, e))) + val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = htlcFailure.failure assert(failingNode == e) assert(decryptedFailure == failure) } @@ -676,33 +708,33 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val (_, route, recipient) = longBlindedHops(hex"0451") val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, 1.0) val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, 1.0, payment.cmd.fundingFee_opt) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) + val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty) val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, 1.0, None) - val Right(ChannelRelayPacket(_, payload_c, packet_d)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) + val Right(ChannelRelayPacket(_, payload_c, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional)) val pathKey_d = payload_c.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, Some(pathKey_d), 1.0, None) - val Right(ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) + val Right(ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional)) val pathKey_e = payload_d.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, Some(pathKey_e), 1.0, None) - val Right(FinalPacket(_, payload_e)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional)) + val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional)) assert(payload_e.isInstanceOf[FinalPayload.Blinded]) // nodes after the introduction node cannot send `update_fail_htlc` messages - val Right(fail_e: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_e.privateKey, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(TemporaryNodeFailure())), add_e) + val Right(fail_e: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_e.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(TemporaryNodeFailure()), None), add_e) assert(fail_e.id == add_e.id) assert(fail_e.onionHash == Sphinx.hash(add_e.onionRoutingPacket)) assert(fail_e.failureCode == InvalidOnionBlinding(fail_e.onionHash).code) - val Right(fail_d: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, FailureReason.LocalFailure(UnknownNextPeer())), add_d) + val Right(fail_d: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_d.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_d.id, FailureReason.LocalFailure(UnknownNextPeer()), None), add_d) assert(fail_d.id == add_d.id) assert(fail_d.onionHash == Sphinx.hash(add_d.onionRoutingPacket)) assert(fail_d.failureCode == InvalidOnionBlinding(fail_d.onionHash).code) // only the introduction node is allowed to send an `update_fail_htlc` message val failure = InvalidOnionBlinding(Sphinx.hash(add_c.onionRoutingPacket)) - val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, FailureReason.LocalFailure(failure)), add_c) + val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, useAttributableFailures = false, CMD_FAIL_HTLC(add_c.id, FailureReason.LocalFailure(failure), None), add_c) assert(fail_c.id == add_c.id) - val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason)), add_b) + 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, payment.sharedSecrets) + val Right(Sphinx.DecryptedFailurePacket(failingNode, 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/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index cadf3285a8..fb5ea241ad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -122,7 +122,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 1 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) channel.expectMsgAllOf( - CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), CMD_FAIL_MALFORMED_HTLC(4, ByteVector32.Zeroes, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 24, commit = true) ) channel.expectNoMessage(100 millis) @@ -130,15 +130,15 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 2 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels(1).commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels(1).commitments))) channel.expectMsgAllOf( - CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), - CMD_FAIL_HTLC(4, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), + CMD_FAIL_HTLC(4, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true) ) channel.expectNoMessage(100 millis) // let's assume that channel 1 was disconnected before having signed the fails, and gets connected again: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) channel.expectMsgAllOf( - CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), CMD_FAIL_MALFORMED_HTLC(4, ByteVector32.Zeroes, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 24, commit = true) ) channel.expectNoMessage(100 millis) @@ -225,10 +225,10 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 1 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) val expected1 = Set( - CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), CMD_FULFILL_HTLC(3, preimage, commit = true), CMD_FULFILL_HTLC(5, preimage, commit = true), - CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true) ) val received1 = expected1.map(_ => channel.expectMsgType[Command]) assert(received1 == expected1) @@ -237,10 +237,10 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 2 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels(1).channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels(1).commitments))) val expected2 = Set( - CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), - CMD_FAIL_HTLC(3, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), + CMD_FAIL_HTLC(3, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true), CMD_FULFILL_HTLC(4, preimage, commit = true), - CMD_FAIL_HTLC(9, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(9, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true) ) val received2 = expected2.map(_ => channel.expectMsgType[Command]) assert(received2 == expected2) @@ -447,8 +447,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit system.eventStream.publish(ChannelStateChanged(channel_upstream_3.ref, data_upstream_3.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(data_upstream_3.commitments))) // Payment 1 should fail instantly. - channel_upstream_1.expectMsg(CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) - channel_upstream_2.expectMsg(CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + channel_upstream_1.expectMsg(CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) + channel_upstream_2.expectMsg(CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) channel_upstream_1.expectNoMessage(100 millis) channel_upstream_2.expectNoMessage(100 millis) @@ -475,7 +475,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit ) val channelData = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab, Map.empty) nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FULFILL_HTLC(1, randomBytes32())) - nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FAIL_HTLC(4, FailureReason.LocalFailure(PermanentChannelFailure()))) + nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FAIL_HTLC(4, FailureReason.LocalFailure(PermanentChannelFailure()), None)) val (_, postRestart) = f.createRelayer(nodeParams) postRestart ! PostRestartHtlcCleaner.Init(List(channelData)) @@ -593,7 +593,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1)) val fails = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil assert(fails.toSet == testCase.upstream_1.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) }.toSet) sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1)) @@ -605,7 +605,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2)) register.expectMsg(testCase.upstream_2.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) }.head) register.expectNoMessage(100 millis) @@ -736,7 +736,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // Standard channel goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, c.commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(c.commitments))) - channel.expectMsg(CMD_FAIL_HTLC(1L, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + channel.expectMsg(CMD_FAIL_HTLC(1L, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) channel.expectNoMessage(100 millis) } @@ -754,8 +754,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit } // @formatter:on - val cmd1 = CMD_FAIL_HTLC(id = 0L, reason = FailureReason.EncryptedDownstreamFailure(ByteVector.empty), replyTo_opt = None) - val cmd2 = CMD_FAIL_HTLC(id = 1L, reason = FailureReason.EncryptedDownstreamFailure(ByteVector.empty), replyTo_opt = None) + val cmd1 = CMD_FAIL_HTLC(id = 0L, reason = FailureReason.EncryptedDownstreamFailure(ByteVector.empty, None), None, replyTo_opt = None) + val cmd2 = CMD_FAIL_HTLC(id = 1L, reason = FailureReason.EncryptedDownstreamFailure(ByteVector.empty, None), None, replyTo_opt = None) val nodeParams1 = nodeParams.copy(pluginParams = List(pluginParams)) nodeParams1.db.pendingCommands.addSettlementCommand(channelId_ab_1, cmd1) nodeParams1.db.pendingCommands.addSettlementCommand(channelId_ab_1, cmd2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 8828fe117e..d1c9eb2134 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -86,9 +86,9 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a } } - def expectFwdFail(register: TestProbe[Any], channelId: ByteVector32, cmd: channel.Command): Register.Forward[channel.Command] = { - val fwd = register.expectMessageType[Register.Forward[channel.Command]] - assert(fwd.message == cmd) + def expectFwdFail(register: TestProbe[Any], channelId: ByteVector32, cmd: CMD_FAIL_HTLC): Register.Forward[CMD_FAIL_HTLC] = { + val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] + assert(fwd.message.copy(htlcReceivedAt_opt = None) == cmd.copy(htlcReceivedAt_opt = None)) assert(fwd.channelId == channelId) fwd } @@ -116,7 +116,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a if (success) { expectFwdAdd(register, lcu.channelId, outgoingAmount, outgoingExpiry, 7) } else { - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } } @@ -240,7 +240,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a cleanUpWakeUpActors(peerReadyManager, switchboard) // We fail without attempting on-the-fly funding. - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } test("relay blinded payment (on-the-fly funding failed)", Tag(wakeUpEnabled), Tag(onTheFlyFunding)) { f => @@ -265,7 +265,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val fwdNodeId = register.expectMessageType[ForwardNodeId[Peer.ProposeOnTheFlyFunding]] assert(fwdNodeId.nodeId == outgoingNodeId) fwdNodeId.replyTo ! Register.ForwardNodeIdFailure(fwdNodeId) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } test("relay blinded payment (on-the-fly funding not attempted)", Tag(wakeUpEnabled), Tag(onTheFlyFunding)) { f => @@ -292,7 +292,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, TooManyAcceptedHtlcs(channelIds(realScid1), 10), Some(u.channelUpdate)) // We fail without attempting on-the-fly funding. - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) } test("relay with retries") { f => @@ -322,7 +322,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(realScid1), 1000000000 msat, 1516977616 msat), Some(u1.channelUpdate)) // the relayer should give up - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(u1.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(u1.channelUpdate))), None, commit = true)) } test("fail to relay when we have no channel_update for the next channel") { f => @@ -333,7 +333,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } test("fail to relay when register returns an error") { f => @@ -349,7 +349,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) fwd.replyTo ! Register.ForwardFailure(fwd) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } test("fail to relay when the channel is advertised as unusable (down)") { f => @@ -364,7 +364,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelDown(d) channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), None, commit = true)) } test("fail to relay when channel is disabled") { f => @@ -377,7 +377,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(ChannelDisabled(u.channelUpdate.messageFlags, u.channelUpdate.channelFlags, Some(u.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(ChannelDisabled(u.channelUpdate.messageFlags, u.channelUpdate.channelFlags, Some(u.channelUpdate))), None, commit = true)) } test("fail to relay when amount is below minimum") { f => @@ -390,7 +390,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(AmountBelowMinimum(outgoingAmount, Some(u.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(AmountBelowMinimum(outgoingAmount, Some(u.channelUpdate))), None, commit = true)) } test("fail to relay blinded payment") { f => @@ -466,7 +466,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(r.outgoingCltv, Some(u.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(r.outgoingCltv, Some(u.channelUpdate))), None, commit = true)) } test("fail to relay when fee is insufficient") { f => @@ -479,7 +479,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(u.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(u.channelUpdate))), None, commit = true)) } test("relay that would fail (fee insufficient) with a recent channel update but succeed with the previous update") { f => @@ -510,7 +510,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) // relay fails because the current update (u3) with higher fees occurred more than 10 minutes ago - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(u3.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(u3.channelUpdate))), None, commit = true)) } test("fail to relay when there is a local error") { f => @@ -539,7 +539,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, 7) fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, testCase.exc, Some(testCase.update)) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(testCase.failure), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(testCase.failure), None, commit = true)) } } @@ -586,7 +586,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val cmd4 = expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, r.amountToForward, r.outgoingCltv, 5).message cmd4.replyTo ! RES_ADD_FAILED(cmd4, HtlcValueTooHighInFlight(randomBytes32(), 100000000 msat, 100000000 msat), Some(channelUpdates(ShortChannelId(11111)).channelUpdate)) // all the suitable channels have been tried - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(channelUpdates(ShortChannelId(12345)).channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(channelUpdates(ShortChannelId(12345)).channelUpdate))), None, commit = true)) } { // higher amount payment (have to increased incoming htlc amount for fees to be sufficient) @@ -621,7 +621,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val payload = ChannelRelay.Standard(ShortChannelId(12345), 998900 msat, CltvExpiry(61)) val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId) - expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(CltvExpiry(61), Some(channelUpdates(ShortChannelId(12345)).channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(CltvExpiry(61), Some(channelUpdates(ShortChannelId(12345)).channelUpdate))), None, commit = true)) } } @@ -634,14 +634,14 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val u_disabled = createLocalUpdate(channelId1, enabled = false) val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, 1.0, None) - case class TestCase(result: HtlcResult, cmd: channel.HtlcSettlementCommand) + case class TestCase(result: HtlcResult, cmd: CMD_FAIL_HTLC) val testCases = Seq( - TestCase(HtlcResult.RemoteFail(UpdateFailHtlc(channelId1, downstream_htlc.id, hex"deadbeef")), CMD_FAIL_HTLC(r.add.id, FailureReason.EncryptedDownstreamFailure(hex"deadbeef"), commit = true)), - TestCase(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(channelId1, downstream_htlc.id, ByteVector32.One, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(InvalidOnionHmac(ByteVector32.One)), commit = true)), - TestCase(HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId1, downstream_htlc)), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true)), - TestCase(HtlcResult.DisconnectedBeforeSigned(u_disabled.channelUpdate), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(u_disabled.channelUpdate))), commit = true)), - TestCase(HtlcResult.ChannelFailureBeforeSigned, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true)) + TestCase(HtlcResult.RemoteFail(UpdateFailHtlc(channelId1, downstream_htlc.id, hex"deadbeef")), CMD_FAIL_HTLC(r.add.id, FailureReason.EncryptedDownstreamFailure(hex"deadbeef", None), None, commit = true)), + TestCase(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(channelId1, downstream_htlc.id, ByteVector32.One, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(InvalidOnionHmac(ByteVector32.One)), None, commit = true)), + TestCase(HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId1, downstream_htlc)), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(PermanentChannelFailure()), None, commit = true)), + TestCase(HtlcResult.DisconnectedBeforeSigned(u_disabled.channelUpdate), CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(u_disabled.channelUpdate))), None, commit = true)), + TestCase(HtlcResult.ChannelFailureBeforeSigned, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(PermanentChannelFailure()), None, commit = true)) ) testCases.foreach { testCase => @@ -727,7 +727,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a assert(fwd2.message.r == paymentPreimage) val paymentRelayed = eventListener.expectMessageType[ChannelPaymentRelayed] - assert(paymentRelayed.copy(startedAt = 0 unixms, settledAt = 0 unixms) == ChannelPaymentRelayed(r.add.amountMsat, r.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, startedAt = 0 unixms, settledAt = 0 unixms)) + assert(paymentRelayed.copy(receivedAt = 0 unixms, settledAt = 0 unixms) == ChannelPaymentRelayed(r.add.amountMsat, r.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, receivedAt = 0 unixms, settledAt = 0 unixms)) } } @@ -859,7 +859,7 @@ object ChannelRelayerSpec { } val tlvs = TlvStream(Set[Option[UpdateAddHtlcTlv]](nextPathKey_opt, Some(UpdateAddHtlcTlv.Endorsement(endorsementIn))).flatten) val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, amountIn, paymentHash, expiryIn, emptyOnionPacket, tlvs) - ChannelRelayPacket(add_ab, payload, emptyOnionPacket) + ChannelRelayPacket(add_ab, payload, emptyOnionPacket, TimestampMilli.now()) } def createAliases(channelId: ByteVector32): ShortIdAliases = { 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 520e044e57..e7a146a99f 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 @@ -230,7 +230,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]](30 seconds) assert(fwd.channelId == p.add.channelId) val failure = FailureReason.LocalFailure(PaymentTimeout()) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, failure, commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, failure, Some(p.receivedAt), commit = true)) } parent.expectMessageType[NodeRelayer.RelayComplete] @@ -244,11 +244,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // We send all the parts of a mpp incomingMultiPart.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) // and then one extra + val extraReceivedAt = TimestampMilli.now() val extra = IncomingPaymentPacket.RelayToTrampolinePacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket, None, 1.0, None), FinalPayload.Standard.createPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None), IntermediatePayload.NodeRelay.Standard(outgoingAmount, outgoingExpiry, outgoingNodeId), - createTrampolinePacket(outgoingAmount, outgoingExpiry)) + createTrampolinePacket(outgoingAmount, outgoingExpiry), + extraReceivedAt) nodeRelayer ! NodeRelay.Relay(extra, randomKey().publicKey) val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis) @@ -258,7 +260,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == extra.add.channelId) val failure = FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(extra.add.amountMsat, nodeParams.currentBlockHeight)) - assert(fwd.message == CMD_FAIL_HTLC(extra.add.id, failure, commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(extra.add.id, failure, Some(extraReceivedAt), commit = true)) register.expectNoMessage(100 millis) } @@ -279,30 +281,34 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl validateOutgoingPayment(outgoingPayment) // Receive new extraneous multi-part HTLC. + val receivedAt1 = TimestampMilli.now() val i1 = IncomingPaymentPacket.RelayToTrampolinePacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket, None, 1.0, None), FinalPayload.Standard.createPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None), IntermediatePayload.NodeRelay.Standard(outgoingAmount, outgoingExpiry, outgoingNodeId), - createTrampolinePacket(outgoingAmount, outgoingExpiry)) + createTrampolinePacket(outgoingAmount, outgoingExpiry), + receivedAt1) nodeRelayer ! NodeRelay.Relay(i1, randomKey().publicKey) val fwd1 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd1.channelId == i1.add.channelId) val failure1 = FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)) - assert(fwd1.message == CMD_FAIL_HTLC(i1.add.id, failure1, commit = true)) + assert(fwd1.message == CMD_FAIL_HTLC(i1.add.id, failure1, Some(receivedAt1), commit = true)) // Receive new HTLC with different details, but for the same payment hash. + val receivedAt2 = TimestampMilli.now() + 1.millis val i2 = IncomingPaymentPacket.RelayToTrampolinePacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1500 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket, None, 1.0, None), PaymentOnion.FinalPayload.Standard.createPayload(1500 msat, 1500 msat, CltvExpiry(499990), incomingSecret, None), IntermediatePayload.NodeRelay.Standard(1250 msat, outgoingExpiry, outgoingNodeId), - createTrampolinePacket(outgoingAmount, outgoingExpiry)) + createTrampolinePacket(outgoingAmount, outgoingExpiry), + receivedAt2) nodeRelayer ! NodeRelay.Relay(i2, randomKey().publicKey) val fwd2 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd1.channelId == i1.add.channelId) val failure2 = FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1500 msat, nodeParams.currentBlockHeight)) - assert(fwd2.message == CMD_FAIL_HTLC(i2.add.id, failure2, commit = true)) + assert(fwd2.message == CMD_FAIL_HTLC(i2.add.id, failure2, Some(receivedAt2), commit = true)) register.expectNoMessage(100 millis) } @@ -312,13 +318,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val expiryIn = CltvExpiry(500000) // not ok (delta = 100) val expiryOut = CltvExpiry(499900) - val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut) + val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut, TimestampMilli.now()) val (nodeRelayer, _) = f.createNodeRelay(p) nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), Some(p.receivedAt), commit = true)) register.expectNoMessage(100 millis) } @@ -328,13 +334,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val expiryIn = CltvExpiry(500000) val expiryOut = CltvExpiry(300000) // not ok (chain height = 400000) - val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut) + val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut, TimestampMilli.now()) val (nodeRelayer, _) = f.createNodeRelay(p) nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), Some(p.receivedAt), commit = true)) register.expectNoMessage(100 millis) } @@ -346,8 +352,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val expiryIn2 = CltvExpiry(500000) // not ok (delta = 100) val expiryOut = CltvExpiry(499900) val p = Seq( - createValidIncomingPacket(2000000 msat, 3000000 msat, expiryIn1, 2100000 msat, expiryOut), - createValidIncomingPacket(1000000 msat, 3000000 msat, expiryIn2, 2100000 msat, expiryOut) + createValidIncomingPacket(2000000 msat, 3000000 msat, expiryIn1, 2100000 msat, expiryOut, TimestampMilli(10)), + createValidIncomingPacket(1000000 msat, 3000000 msat, expiryIn2, 2100000 msat, expiryOut, TimestampMilli(20)) ) val (nodeRelayer, _) = f.createNodeRelay(p.head) p.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) @@ -355,7 +361,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), Some(p.receivedAt), commit = true)) } register.expectNoMessage(100 millis) @@ -401,13 +407,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl test("fail to relay when fees are insufficient (single-part)") { f => import f._ - val p = createValidIncomingPacket(2000000 msat, 2000000 msat, CltvExpiry(500000), 1999000 msat, CltvExpiry(490000)) + val p = createValidIncomingPacket(2000000 msat, 2000000 msat, CltvExpiry(500000), 1999000 msat, CltvExpiry(490000), TimestampMilli.now()) val (nodeRelayer, _) = f.createNodeRelay(p) nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), Some(p.receivedAt), commit = true)) register.expectNoMessage(100 millis) } @@ -416,8 +422,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import f._ val p = Seq( - createValidIncomingPacket(2000000 msat, 3000000 msat, CltvExpiry(500000), 2999000 msat, CltvExpiry(400000)), - createValidIncomingPacket(1000000 msat, 3000000 msat, CltvExpiry(500000), 2999000 msat, CltvExpiry(400000)) + createValidIncomingPacket(2000000 msat, 3000000 msat, CltvExpiry(500000), 2999000 msat, CltvExpiry(400000), TimestampMilli(153)), + createValidIncomingPacket(1000000 msat, 3000000 msat, CltvExpiry(500000), 2999000 msat, CltvExpiry(400000), TimestampMilli(486)) ) val (nodeRelayer, _) = f.createNodeRelay(p.head) p.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) @@ -425,7 +431,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), Some(p.receivedAt), commit = true)) } register.expectNoMessage(100 millis) @@ -434,13 +440,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl test("fail to relay when amount is 0 (single-part)") { f => import f._ - val p = createValidIncomingPacket(5000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000)) + val p = createValidIncomingPacket(5000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000), TimestampMilli.now()) val (nodeRelayer, _) = f.createNodeRelay(p) nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(InvalidOnionPayload(UInt64(2), 0)), Some(p.receivedAt), commit = true)) register.expectNoMessage(100 millis) } @@ -449,8 +455,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import f._ val p = Seq( - createValidIncomingPacket(4000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000)), - createValidIncomingPacket(1000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000)) + createValidIncomingPacket(4000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000), TimestampMilli(7)), + createValidIncomingPacket(1000000 msat, 5000000 msat, CltvExpiry(500000), 0 msat, CltvExpiry(490000), TimestampMilli(9)) ) val (nodeRelayer, _) = f.createNodeRelay(p.head) p.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) @@ -458,7 +464,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(InvalidOnionPayload(UInt64(2), 0)), Some(p.receivedAt), commit = true)) } register.expectNoMessage(100 millis) @@ -483,7 +489,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), Some(p.receivedAt), commit = true)) } register.expectNoMessage(100 millis) @@ -495,8 +501,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val incoming = Seq( - createValidIncomingPacket(outgoingAmount, outgoingAmount * 2, CltvExpiry(500000), outgoingAmount, outgoingExpiry), - createValidIncomingPacket(outgoingAmount, outgoingAmount * 2, CltvExpiry(500000), outgoingAmount, outgoingExpiry), + createValidIncomingPacket(outgoingAmount, outgoingAmount * 2, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(1)), + createValidIncomingPacket(outgoingAmount, outgoingAmount * 2, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(2)), ) val (nodeRelayer, _) = f.createNodeRelay(incoming.head, useRealPaymentFactory = true) incoming.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey)) @@ -511,7 +517,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incoming.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), Some(p.receivedAt), commit = true)) } register.expectNoMessage(100 millis) @@ -537,7 +543,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), Some(p.receivedAt), commit = true)) } register.expectNoMessage(100 millis) @@ -556,13 +562,13 @@ 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, ByteVector.empty) :: Nil + val failures = RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(outgoingAmount, Nil, ByteVector.empty, Nil) :: Nil payFSM ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, failures) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(FinalIncorrectHtlcAmount(42 msat)), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(FinalIncorrectHtlcAmount(42 msat)), Some(p.receivedAt), commit = true)) } register.expectNoMessage(100 millis) @@ -782,7 +788,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), Some(p.receivedAt), commit = true)) } parent.expectMessageType[NodeRelayer.RelayComplete] } @@ -796,7 +802,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, outgoingNodeKey, Left("Some invoice"), CltvExpiryDelta(18), extraHops = List(hints), paymentMetadata = Some(hex"123456"), features = features) val incomingPayments = incomingMultiPart.map(incoming => { val innerPayload = IntermediatePayload.NodeRelay.ToNonTrampoline(incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, invoice) - RelayToNonTrampolinePacket(incoming.add, incoming.outerPayload, innerPayload) + RelayToNonTrampolinePacket(incoming.add, incoming.outerPayload, innerPayload, TimestampMilli.now()) }) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) @@ -844,7 +850,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(!invoice.features.hasFeature(BasicMultiPartPayment)) val incomingPayments = incomingMultiPart.map(incoming => { val innerPayload = IntermediatePayload.NodeRelay.ToNonTrampoline(incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, invoice) - RelayToNonTrampolinePacket(incoming.add, incoming.outerPayload, innerPayload) + RelayToNonTrampolinePacket(incoming.add, incoming.outerPayload, innerPayload, TimestampMilli.now()) }) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming, randomKey().publicKey)) @@ -1009,7 +1015,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), Some(p.receivedAt), commit = true)) } } @@ -1079,7 +1085,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), Some(p.receivedAt), commit = true)) } } @@ -1140,7 +1146,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingPayments.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), Some(p.receivedAt), commit = true)) } } @@ -1194,11 +1200,11 @@ object NodeRelayerSpec { val incomingAmount = 41_000_000 msat val incomingSecret = randomBytes32() val incomingMultiPart = Seq( - createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry, endorsementIn = 6), - createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, endorsementIn = 5), - createValidIncomingPacket(11_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, endorsementIn = 7) + createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(1000), endorsementIn = 6), + createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, TimestampMilli(2000), endorsementIn = 5), + createValidIncomingPacket(11_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, TimestampMilli(3000), endorsementIn = 7) ) - val incomingSinglePart = createValidIncomingPacket(incomingAmount, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry) + val incomingSinglePart = createValidIncomingPacket(incomingAmount, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(5000)) val incomingAsyncPayment: Seq[RelayToTrampolinePacket] = incomingMultiPart.map(p => p.copy(innerPayload = IntermediatePayload.NodeRelay.Standard.createNodeRelayForAsyncPayment(p.innerPayload.amountToForward, p.innerPayload.outgoingCltv, outgoingNodeId))) def asyncTimeoutHeight(nodeParams: NodeParams): BlockHeight = @@ -1216,14 +1222,15 @@ object NodeRelayerSpec { onion.packet } - def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry, endorsementIn: Int = 7): RelayToTrampolinePacket = { + def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry, receivedAt: TimestampMilli, endorsementIn: Int = 7): RelayToTrampolinePacket = { val outerPayload = FinalPayload.Standard.createPayload(amountIn, totalAmountIn, expiryIn, incomingSecret, None) val tlvs = TlvStream[UpdateAddHtlcTlv](UpdateAddHtlcTlv.Endorsement(endorsementIn)) RelayToTrampolinePacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket, tlvs), outerPayload, IntermediatePayload.NodeRelay.Standard(amountOut, expiryOut, outgoingNodeId), - createTrampolinePacket(amountOut, expiryOut)) + createTrampolinePacket(amountOut, expiryOut), + receivedAt) } def createPartialIncomingPacket(paymentHash: ByteVector32, paymentSecret: ByteVector32): RelayToTrampolinePacket = { @@ -1233,7 +1240,8 @@ object NodeRelayerSpec { UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket, None, 1.0, None), FinalPayload.Standard.createPayload(amountIn, incomingAmount, expiryIn, paymentSecret, None), IntermediatePayload.NodeRelay.Standard(outgoingAmount, expiryOut, outgoingNodeId), - createTrampolinePacket(outgoingAmount, expiryOut)) + createTrampolinePacket(outgoingAmount, expiryOut), + TimestampMilli.now()) } def createPaymentBlindedRoute(nodeId: PublicKey, sessionKey: PrivateKey = randomKey(), pathId: ByteVector = randomBytes32()): PaymentBlindedRoute = { @@ -1255,7 +1263,7 @@ object NodeRelayerSpec { val invoice = Bolt12Invoice(request, randomBytes32(), outgoingNodeKey, 300 seconds, features, Seq(paymentBlindedRoute)) incomingMultiPart.map(incoming => { val innerPayload = IntermediatePayload.NodeRelay.ToBlindedPaths(incoming.innerPayload.amountToForward, outgoingExpiry, invoice) - RelayToBlindedPathsPacket(incoming.add, incoming.outerPayload, innerPayload) + RelayToBlindedPathsPacket(incoming.add, incoming.outerPayload, innerPayload, incoming.receivedAt) }) } @@ -1271,7 +1279,7 @@ object NodeRelayerSpec { val invoice = Bolt12Invoice(request, randomBytes32(), outgoingNodeKey, 300 seconds, features, Seq(PaymentBlindedRoute(route, paymentInfo))) incomingMultiPart.map(incoming => { val innerPayload = IntermediatePayload.NodeRelay.ToBlindedPaths(incoming.innerPayload.amountToForward, outgoingExpiry, invoice) - RelayToBlindedPathsPacket(incoming.add, incoming.outerPayload, innerPayload) + RelayToBlindedPathsPacket(incoming.add, incoming.outerPayload, innerPayload, incoming.receivedAt) }) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala index cdbaeab475..aa4731b2f2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala @@ -239,7 +239,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwd1 = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd1.channelId == upstream1.add.channelId) assert(fwd1.message.id == upstream1.add.id) - assert(fwd1.message.reason == FailureReason.EncryptedDownstreamFailure(fail1.reason)) + assert(fwd1.message.reason == FailureReason.EncryptedDownstreamFailure(fail1.reason, None)) register.expectNoMessage(100 millis) val fail2 = WillFailHtlc(willAdd2.id, paymentHash, randomBytes(50)) @@ -247,7 +247,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwd2 = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd2.channelId == upstream2.add.channelId) assert(fwd2.message.id == upstream2.add.id) - assert(fwd2.message.reason == FailureReason.EncryptedDownstreamFailure(fail2.reason)) + assert(fwd2.message.reason == FailureReason.EncryptedDownstreamFailure(fail2.reason, None)) val fail3 = WillFailMalformedHtlc(willAdd3.id, paymentHash, randomBytes32(), InvalidOnionHmac(randomBytes32()).code) peerConnection.send(peer, fail3) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala index e6956f1d12..3fd80430b5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.internal import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.{TimestampMilli, UInt64, randomBytes} import fr.acinq.eclair.channel._ import fr.acinq.eclair.wire.protocol._ import org.scalatest.funsuite.AnyFunSuite @@ -32,9 +32,10 @@ class CommandCodecsSpec extends AnyFunSuite { test("encode/decode all settlement commands") { val testCases: Map[HtlcSettlementCommand, ByteVector] = Map( CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927")) -> hex"0000 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927", - CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44")) -> hex"0004 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", - CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure())) -> hex"0004 00000000000000fd 01 0002 2002", - CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef")))))) -> hex"0004 00000000000000fd 01 0008 2002 1104deadbeef", + CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), Some(TimestampMilli(123456))) -> hex"0005 000000000000a5d8 02 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44 00 ff 000000000001e240", + CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", Some(hex"c8e7a432d1db82765a5e4cecf360b439ff9cabe795f470249065a1b8d893c9ac5142092e9260b073caadc7097d3d0aa6b5f8181c02b8eb9ecb9c17a9867e8e533766e79c00844321a7a15afcd562cefbde4cc7a66865b57192f770f68260cc09e133876181921018ade17ac1bdca027af5073b5e354cbe888651a0592a228de9019b1136b702e56424c84e01ec60cee5df2ad7179196809bcbf58e17f91fd851f91fd2a5be1ea5efb43ea18d1a5b8ad2d1fe69a9f29fbea84565c34ca3088e848e5c4297f7bacea917f332a311beb365f3f131d21871120fd3fdeefb0566cb56e8dac56cdb1eee4ebb2bfcdc954326566cfb42481e7ef5fb3031ac9190e8e02f35a9430ce7cf5167f75e5c016056e2dba3022acabfe20a6891f57cfcf0abf09102b7af1b91223badca2eb865e8a523b141dcf91955631e4efd7e9664205e89aaa2282826ac65e9651620dc3392231f8f28821271da0ce9c5eb3f145837aefde0e5b33b5cb8f847de6caa51b3488baedbb706012c9b7034919f23b7c043d3e484f4be9ab72b9b37985c34c21b5ffcbb40e9b11fe83661cef912c97f5a6b4cce76518a0245d45d0fb2844b2853457a982a9418fa83934fead109f8a38ac23c3a03bbb32d573d2349bb2228c8a53efc65e9165526b53034e53d4f8540960129657b88e28e75c9f8d26f48c4d2cc86006456f03e0bff14262e97f94d64e58369798e43bb89f6f1ae731301eded7bce4ec21613670be6939ad17b8b6d4a9ca059cc5ed33dcc7c7608dda6e2810c7d76b20fa23f5e8adb91fa895ef2ff030086168e16bcc4a4376d36f2c169e5821bb40316df88e456a8b99057f130b8ad1f097e7c9f81d64c816531a325e6b517de4022bea321a41a92386ce50ca271ff1d2ebaf0694e57545e624ef3f76eae1454314136276bf1bab91e3fcdf541eb60052fc65932505be888e3ec1de782e27a3689727128cb1018fbd4ca7b51916ba05280f1004cf5bfdde6159453e2936b76842c1d978e34d0a5f65ba2a27dd235538c2875a1ca9433b7a799aa30e28facb6603e8644da1ae9a8cf169df35f905a366aada5e0c4fc1a7f9cb36f75a12983fd9e43d3339d506d37aebd9886a52f98cc330c812605508525d8c8ad5b93a9c08c5d41123dbd7e15644f61a9ab5758aa2615cc1781d997f4d2177d4b56c4be0276e67debb4cd01e398e8a6d9d3ceac030a01235b965b1a733f2710251bb1638cdb77894667aecedb1bd56b8e9979f938a4f9a66a9da44a6d6727fdfa01641b021d0d89061370a0cb2fa68d1f242a")), Some(TimestampMilli(123456))) -> hex"0005 000000000000a5d8 02 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44 ff c8e7a432d1db82765a5e4cecf360b439ff9cabe795f470249065a1b8d893c9ac5142092e9260b073caadc7097d3d0aa6b5f8181c02b8eb9ecb9c17a9867e8e533766e79c00844321a7a15afcd562cefbde4cc7a66865b57192f770f68260cc09e133876181921018ade17ac1bdca027af5073b5e354cbe888651a0592a228de9019b1136b702e56424c84e01ec60cee5df2ad7179196809bcbf58e17f91fd851f91fd2a5be1ea5efb43ea18d1a5b8ad2d1fe69a9f29fbea84565c34ca3088e848e5c4297f7bacea917f332a311beb365f3f131d21871120fd3fdeefb0566cb56e8dac56cdb1eee4ebb2bfcdc954326566cfb42481e7ef5fb3031ac9190e8e02f35a9430ce7cf5167f75e5c016056e2dba3022acabfe20a6891f57cfcf0abf09102b7af1b91223badca2eb865e8a523b141dcf91955631e4efd7e9664205e89aaa2282826ac65e9651620dc3392231f8f28821271da0ce9c5eb3f145837aefde0e5b33b5cb8f847de6caa51b3488baedbb706012c9b7034919f23b7c043d3e484f4be9ab72b9b37985c34c21b5ffcbb40e9b11fe83661cef912c97f5a6b4cce76518a0245d45d0fb2844b2853457a982a9418fa83934fead109f8a38ac23c3a03bbb32d573d2349bb2228c8a53efc65e9165526b53034e53d4f8540960129657b88e28e75c9f8d26f48c4d2cc86006456f03e0bff14262e97f94d64e58369798e43bb89f6f1ae731301eded7bce4ec21613670be6939ad17b8b6d4a9ca059cc5ed33dcc7c7608dda6e2810c7d76b20fa23f5e8adb91fa895ef2ff030086168e16bcc4a4376d36f2c169e5821bb40316df88e456a8b99057f130b8ad1f097e7c9f81d64c816531a325e6b517de4022bea321a41a92386ce50ca271ff1d2ebaf0694e57545e624ef3f76eae1454314136276bf1bab91e3fcdf541eb60052fc65932505be888e3ec1de782e27a3689727128cb1018fbd4ca7b51916ba05280f1004cf5bfdde6159453e2936b76842c1d978e34d0a5f65ba2a27dd235538c2875a1ca9433b7a799aa30e28facb6603e8644da1ae9a8cf169df35f905a366aada5e0c4fc1a7f9cb36f75a12983fd9e43d3339d506d37aebd9886a52f98cc330c812605508525d8c8ad5b93a9c08c5d41123dbd7e15644f61a9ab5758aa2615cc1781d997f4d2177d4b56c4be0276e67debb4cd01e398e8a6d9d3ceac030a01235b965b1a733f2710251bb1638cdb77894667aecedb1bd56b8e9979f938a4f9a66a9da44a6d6727fdfa01641b021d0d89061370a0cb2fa68d1f242a ff 000000000001e240", + CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), Some(TimestampMilli(123))) -> hex"0005 00000000000000fd 01 0002 2002 ff 000000000000007b", + CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), Some(TimestampMilli(456))) -> hex"0005 00000000000000fd 01 0008 2002 1104deadbeef ff 00000000000001c8", CMD_FAIL_MALFORMED_HTLC(7984, ByteVector32(hex"17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f3"), FailureMessageCodecs.BADONION) -> hex"0002 0000000000001f30 17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f38000", ) @@ -51,12 +52,15 @@ class CommandCodecsSpec extends AnyFunSuite { val data123 = hex"fea75bb8cf45349eb544d8da832af5af30eefa671ec27cf2e4867bacada2dbe00a6ce5141164aa153ac8b4b25c75c3af15c4b5cb6a293607751a079bc546da17f654b76a74bc57b6b21ed73d2d3909f3682f01b85418a0f0ecddb759e9481d4563a572ac1ddcb77c64ae167d8dfbd889703cb5c33b4b9636bad472" val testCases = Map( hex"0000 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb" -> CMD_FULFILL_HTLC(42, data32, commit = false, None), - hex"0001 000000000000002a003dff53addc67a29a4f5aa26c6d41957ad798777d338f613e7972433dd656d16df00536728a08b2550a9d645a592e3ae1d78ae25ae5b5149b03ba8d03cde2a36d0bfb2a5bb53a5e2bdb590f6b9e969c84f9b41780dc2a0c5078766edbacf4a40ea2b1d2b9560eee5bbe32570b3ec6fdec44b81e5ae19da5cb1b5d6a3900" -> CMD_FAIL_HTLC(42, FailureReason.EncryptedDownstreamFailure(data123), None, commit = false, None), - hex"0001 000000000000002a900100" -> CMD_FAIL_HTLC(42, FailureReason.LocalFailure(TemporaryNodeFailure())), - hex"0003 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44" -> CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44")), - hex"0003 00000000000000fd ff 0002 2002" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure())), - hex"0003 00000000000000fd ff 0008 2002 1104deadbeef" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef")))))), + hex"0001 000000000000002a003dff53addc67a29a4f5aa26c6d41957ad798777d338f613e7972433dd656d16df00536728a08b2550a9d645a592e3ae1d78ae25ae5b5149b03ba8d03cde2a36d0bfb2a5bb53a5e2bdb590f6b9e969c84f9b41780dc2a0c5078766edbacf4a40ea2b1d2b9560eee5bbe32570b3ec6fdec44b81e5ae19da5cb1b5d6a3900" -> CMD_FAIL_HTLC(42, FailureReason.EncryptedDownstreamFailure(data123, None), None, None, commit = false, None), + hex"0001 000000000000002a900100" -> CMD_FAIL_HTLC(42, FailureReason.LocalFailure(TemporaryNodeFailure()), None), + hex"0003 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44" -> CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), None), + hex"0003 00000000000000fd ff 0002 2002" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), None), + hex"0003 00000000000000fd ff 0008 2002 1104deadbeef" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), None), hex"0002 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb01c8" -> CMD_FAIL_MALFORMED_HTLC(42, data32, 456, commit = false, None), + hex"0004 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44" -> CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), None), + hex"0004 00000000000000fd 01 0002 2002" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), None), + hex"0004 00000000000000fd 01 0008 2002 1104deadbeef" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), None), ) testCases.foreach { case (bin, command) => diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 348d4b7d07..f1206138e4 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -1187,7 +1187,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM wsClient.expectMessage(expectedSerializedPs) val prel = ChannelPaymentRelayed(21 msat, 20 msat, ByteVector32.Zeroes, ByteVector32.Zeroes, ByteVector32.One, TimestampMilli(1553784961048L), TimestampMilli(1553784963659L)) - val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","startedAt":{"iso":"2019-03-28T14:56:01.048Z","unix":1553784961},"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" + val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","receivedAt":{"iso":"2019-03-28T14:56:01.048Z","unix":1553784961},"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" assert(serialization.write(prel) == expectedSerializedPrel) system.eventStream.publish(prel) wsClient.expectMessage(expectedSerializedPrel)