From 2ad22602fd332a32c46c9efff1e90cc962a684a5 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:46:58 +0100 Subject: [PATCH] Refactor Sphinx failures (#2955) * Return unwrapped decryption failure onion When we fail to decrypt an onion failure packet, we should return the result of the unwrapping process. When using trampoline, this will let us properly re-encrypt the failure and relay it upstream to the previous trampoline node, until it reaches the sender. * Refactor HTLC failure creation We refactor the shared secret extraction to a dedicated function. * Refactor HTLC failure reason We previously used an `Either[ByteVector, FailureMessage]` to encode: - a downstream error that we couldn't decrypt and must re-wrap (left) - a local error that we must encrypt (right) This won't be sufficient for trampoline, because we will need to handle the following cases: - a downstream error that we couldn't decrypt and must re-wrap - a local error for the node who created the *outer* onion (which we encrypt with the sphinx shared secret of the outer onion) - a local error for the node who created the *trampoline* onion (which we encrypt with the sphinx shared secret of the trampoline onion and then with the shared secret of the outer onion) We thus introduce a trait, which currently only contains the first two cases. We will extend this trait when adding support for trampoline failures. This is a pure refactoring without any behavior changes so far, which will simplify the future trampoline changes. * Include trampoline onion in final trampoline payloads When we receive a (non-blinded) payment that uses trampoline, we keep the trampoline onion to be able to distinguish this payment from a non-trampoline payment. --- docs/Guides.md | 1 - .../fr/acinq/eclair/channel/ChannelData.scala | 4 +- .../fr/acinq/eclair/channel/fsm/Channel.scala | 6 +-- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 27 +++++++---- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 2 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 6 +-- .../fr/acinq/eclair/payment/Monitoring.scala | 7 +-- .../acinq/eclair/payment/PaymentEvents.scala | 4 +- .../acinq/eclair/payment/PaymentPacket.scala | 27 +++++++---- .../payment/receive/MultiPartHandler.scala | 16 +++---- .../eclair/payment/relay/ChannelRelay.scala | 30 ++++++------ .../eclair/payment/relay/NodeRelay.scala | 2 +- .../payment/relay/OnTheFlyFunding.scala | 16 ++++--- .../relay/PostRestartHtlcCleaner.scala | 6 +-- .../acinq/eclair/payment/relay/Relayer.scala | 6 +-- .../payment/send/PaymentLifecycle.scala | 37 ++++++++------- .../acinq/eclair/payment/send/Recipient.scala | 2 +- .../eclair/wire/internal/CommandCodecs.scala | 38 +++++++++++++-- .../eclair/wire/protocol/FailureMessage.scala | 15 ++++++ .../eclair/wire/protocol/PaymentOnion.scala | 6 ++- .../eclair/channel/ChannelDataSpec.scala | 6 +-- .../eclair/channel/CommitmentsSpec.scala | 6 +-- .../ChannelStateTestsHelperMethods.scala | 2 +- .../states/e/NormalQuiescentStateSpec.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 18 ++++---- .../channel/states/e/OfflineStateSpec.scala | 2 +- .../channel/states/f/ShutdownStateSpec.scala | 10 ++-- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 18 ++++---- .../eclair/db/PendingCommandsDbSpec.scala | 8 ++-- .../eclair/payment/MultiPartHandlerSpec.scala | 42 ++++++++--------- .../MultiPartPaymentLifecycleSpec.scala | 10 ++-- .../eclair/payment/PaymentLifecycleSpec.scala | 24 ++++++---- .../eclair/payment/PaymentPacketSpec.scala | 43 +++++++++++------ .../payment/PostRestartHtlcCleanerSpec.scala | 34 +++++++------- .../payment/relay/ChannelRelayerSpec.scala | 42 ++++++++--------- .../payment/relay/NodeRelayerSpec.scala | 46 +++++++++---------- .../payment/relay/OnTheFlyFundingSpec.scala | 20 ++++---- .../eclair/payment/relay/RelayerSpec.scala | 4 +- .../wire/internal/CommandCodecsSpec.scala | 15 +++--- 39 files changed, 349 insertions(+), 261 deletions(-) diff --git a/docs/Guides.md b/docs/Guides.md index 91c3f00447..a74315956a 100644 --- a/docs/Guides.md +++ b/docs/Guides.md @@ -7,7 +7,6 @@ This section contains how-to guides for more advanced scenarios: * [Manage Bitcoin Core's private keys](./ManagingBitcoinCoreKeys.md) * [Use Tor with Eclair](./Tor.md) * [Multipart Payments](./MultipartPayments.md) -* [Trampoline Payments](./TrampolinePayments.md) * [Monitoring Eclair](./Monitoring.md) * [PostgreSQL Configuration](./PostgreSQL.md) * [Perform Circular Rebalancing](./CircularRebalancing.md) 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 fb4dc32521..3b0221542d 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 @@ -26,7 +26,7 @@ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningS import fr.acinq.eclair.io.Peer import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, LiquidityAds, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxInitRbf, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureReason, FundingCreated, FundingSigned, Init, LiquidityAds, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxInitRbf, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, TimestampMilli, UInt64} import scodec.bits.ByteVector @@ -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: Either[ByteVector, FailureMessage], 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, 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/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index a9c21e88d7..06a9d83ea9 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 @@ -643,7 +643,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with 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, Right(TemporaryChannelFailure(Some(d.channelUpdate))), commit = true) + self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(d.channelUpdate))), commit = true) case PostRevocationAction.RelayFailure(result) => log.debug("forwarding {} to relayer", result) relayer ! result @@ -1544,11 +1544,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with 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, Right(PermanentChannelFailure()), commit = true) + self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), 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, Right(PermanentChannelFailure()), commit = true) + self ! CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(PermanentChannelFailure()), commit = true) case PostRevocationAction.RelayFailure(result) => log.debug("forwarding {} to relayer", result) relayer ! result 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 5e91e3f338..3cec8ebfa4 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 @@ -272,6 +272,13 @@ object Sphinx extends Logging { */ case class DecryptedFailurePacket(originNode: PublicKey, failureMessage: FailureMessage) + /** + * The downstream failure could not be decrypted. + * + * @param unwrapped encrypted failure packet after unwrapping using our shared secrets. + */ + case class CannotDecryptFailurePacket(unwrapped: ByteVector) + object FailurePacket { /** @@ -314,18 +321,18 @@ object Sphinx extends Logging { * * @param packet failure packet. * @param sharedSecrets nodes shared secrets. - * @return Success(secret, failure message) if the origin of the packet could be identified and the packet - * decrypted, Failure otherwise. + * @return failure message if the origin of the packet could be identified and the packet decrypted, the unwrapped + * failure packet otherwise. */ - def decrypt(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[DecryptedFailurePacket] = Try { + def decrypt(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Either[CannotDecryptFailurePacket, DecryptedFailurePacket] = { @tailrec - def loop(packet: ByteVector, secrets: Seq[(ByteVector32, PublicKey)]): DecryptedFailurePacket = secrets match { - case Nil => throw new RuntimeException(s"couldn't parse error packet=$packet with sharedSecrets=$sharedSecrets") + def loop(packet: ByteVector, secrets: Seq[(ByteVector32, PublicKey)]): Either[CannotDecryptFailurePacket, DecryptedFailurePacket] = secrets match { + case Nil => Left(CannotDecryptFailurePacket(packet)) case (secret, pubkey) :: tail => val packet1 = wrap(packet, secret) val um = generateKey("um", secret) FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match { - case Attempt.Successful(value) => DecryptedFailurePacket(pubkey, value.value) + case Attempt.Successful(value) => Right(DecryptedFailurePacket(pubkey, value.value)) case _ => loop(packet1, tail) } } @@ -359,10 +366,10 @@ object Sphinx extends Logging { case class BlindedHop(blindedPublicKey: PublicKey, encryptedPayload: ByteVector) /** - * @param firstNodeId the first node, not blinded so that the sender can locate it. - * @param firstPathKey blinding tweak that can be used by the introduction node to derive the private key that - * matches the blinded public key. - * @param blindedHops blinded nodes (including the introduction node). + * @param firstNodeId the first node, not blinded so that the sender can locate it. + * @param firstPathKey blinding tweak that can be used by the introduction node to derive the private key that + * matches the blinded public key. + * @param blindedHops blinded nodes (including the introduction node). */ case class BlindedRoute(firstNodeId: EncodedNodeId, firstPathKey: PublicKey, blindedHops: Seq[BlindedHop]) { require(blindedHops.nonEmpty, "blinded route must not be empty") 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 26cd0fa51e..f5fcdfc3fa 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 cb59e6f0b4..e87e03edcc 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 @@ -44,7 +44,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.createBadOnionFailure -import fr.acinq.eclair.wire.protocol.{AddFeeCredit, ChannelTlv, CurrentFeeCredit, Error, HasChannelId, HasTemporaryChannelId, LightningMessage, LiquidityAds, NodeAddress, OnTheFlyFundingFailureMessage, OnionMessage, OnionRoutingPacket, RecommendedFeerates, RoutingMessage, SpliceInit, TemporaryChannelFailure, TlvStream, TxAbort, UnknownMessage, Warning, WillAddHtlc, WillFailHtlc, WillFailMalformedHtlc} +import fr.acinq.eclair.wire.protocol.{AddFeeCredit, ChannelTlv, CurrentFeeCredit, Error, FailureReason, HasChannelId, HasTemporaryChannelId, LightningMessage, LiquidityAds, NodeAddress, OnTheFlyFundingFailureMessage, OnionMessage, OnionRoutingPacket, RecommendedFeerates, RoutingMessage, SpliceInit, TemporaryChannelFailure, TlvStream, TxAbort, UnknownMessage, Warning, WillAddHtlc, WillFailHtlc, WillFailMalformedHtlc} /** * This actor represents a logical peer. There is one [[Peer]] per unique remote node id at all time. @@ -300,8 +300,8 @@ class Peer(val nodeParams: NodeParams, pending.proposed.find(_.htlc.id == msg.id) match { case Some(htlc) => val failure = msg match { - case msg: WillFailHtlc => Left(msg.reason) - case msg: WillFailMalformedHtlc => Right(createBadOnionFailure(msg.onionHash, msg.failureCode)) + case msg: WillFailHtlc => FailureReason.EncryptedDownstreamFailure(msg.reason) + case msg: WillFailMalformedHtlc => FailureReason.LocalFailure(createBadOnionFailure(msg.onionHash, msg.failureCode)) } htlc.createFailureCommands(Some(failure)).foreach { case (channelId, cmd) => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) } val proposed1 = pending.proposed.filterNot(_.htlc.id == msg.id) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala index 856f1e58e8..6aa201aaa9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.channel.CMD_FAIL_HTLC +import fr.acinq.eclair.wire.protocol.FailureReason import kamon.Kamon object Monitoring { @@ -127,14 +128,14 @@ object Monitoring { val Malformed = "MalformedHtlc" def apply(cmdFail: CMD_FAIL_HTLC): String = cmdFail.reason match { - case Left(_) => Remote - case Right(f) => f.getClass.getSimpleName + case _: FailureReason.EncryptedDownstreamFailure => Remote + case FailureReason.LocalFailure(f) => f.getClass.getSimpleName } def apply(pf: PaymentFailure): String = pf match { case LocalFailure(_, _, t) => t.getClass.getSimpleName case RemoteFailure(_, _, e) => e.failureMessage.getClass.getSimpleName - case UnreadableRemoteFailure(_, _) => "UnreadableRemoteFailure" + case _: UnreadableRemoteFailure => "UnreadableRemoteFailure" } } 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 34972e5ea5..c070d9fb7e 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 @@ -150,7 +150,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]) extends PaymentFailure +case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop], failurePacket: ByteVector) extends PaymentFailure object PaymentFailure { @@ -235,7 +235,7 @@ object PaymentFailure { } case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _)) => ignoreNodeOutgoingEdge(nodeId, hops, ignore) - case UnreadableRemoteFailure(_, hops) => + case UnreadableRemoteFailure(_, hops, _) => // We don't know which node is sending garbage, let's blacklist all nodes except: // - 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 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 a23ea69a88..e42ecc9c9a 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 @@ -238,7 +238,8 @@ object IncomingPaymentPacket { case innerPayload => // 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). - Right(FinalPacket(add, FinalPayload.Standard.createPayload(outerPayload.amount, innerPayload.totalAmount, innerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata))) + 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))) } } } @@ -334,14 +335,24 @@ object OutgoingPaymentPacket { } } - private def buildHtlcFailure(nodeSecret: PrivateKey, reason: Either[ByteVector, FailureMessage], add: UpdateAddHtlc): Either[CannotExtractSharedSecret, ByteVector] = { + private def buildHtlcFailure(nodeSecret: PrivateKey, reason: FailureReason, add: UpdateAddHtlc): Either[CannotExtractSharedSecret, ByteVector] = { + 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) + } + }) + } + + /** + * We decrypt the onion again to extract the shared secret used to encrypt onion failures. + * We could avoid this by storing the shared secret after the initial onion decryption, but we would have to store it + * in the database since we must be able to fail HTLCs after restarting our node. + * It's simpler to extract it again from the encrypted onion. + */ + private def extractSharedSecret(nodeSecret: PrivateKey, add: UpdateAddHtlc): Either[CannotExtractSharedSecret, ByteVector32] = { Sphinx.peel(nodeSecret, Some(add.paymentHash), add.onionRoutingPacket) match { - case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) => - val encryptedReason = reason match { - case Left(forwarded) => Sphinx.FailurePacket.wrap(forwarded, sharedSecret) - case Right(failure) => Sphinx.FailurePacket.create(sharedSecret, failure) - } - Right(encryptedReason) + case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) => Right(sharedSecret) case Left(_) => Left(CannotExtractSharedSecret(add.channelId, add)) } } 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 2ffbb2356a..28c5e23796 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 @@ -126,7 +126,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP ctx.self ! ProcessPacket(add, payload, Some(IncomingStandardPayment(invoice, paymentPreimage, PaymentType.KeySend, TimestampMilli.now(), IncomingPaymentStatus.Pending))) case _ => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment() - val cmdFail = CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) } } @@ -146,7 +146,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP case RejectPacket(add, failure) 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, Right(failure), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, cmdFail) case MultiPartPaymentFSM.MultiPartPaymentFailed(paymentHash, failure, parts) if doHandle(paymentHash) => @@ -155,7 +155,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, Right(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), commit = true)) } pendingPayments = pendingPayments - paymentHash } @@ -175,7 +175,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, Right(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), commit = true)) } case None => p match { // NB: this case shouldn't happen unless the sender violated the spec, so it's ok that we take a slightly more @@ -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, Right(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, cmdFail) } }) @@ -219,7 +219,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, Right(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(p.htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(received.amount, nodeParams.currentBlockHeight)), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, cmdFail) } } @@ -521,7 +521,7 @@ object MultiPartHandler { private def validateStandardPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Standard, record: IncomingStandardPayment)(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, Right(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) val commonOk = validateCommon(nodeParams, add, payload, record) val secretOk = validatePaymentSecret(add, payload, record.invoice) if (commonOk && secretOk) None else Some(cmdFail) @@ -529,7 +529,7 @@ object MultiPartHandler { private def validateBlindedPayment(nodeParams: NodeParams, add: UpdateAddHtlc, payload: FinalPayload.Blinded, record: IncomingBlindedPayment)(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, Right(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) val commonOk = validateCommon(nodeParams, add, payload, record) if (commonOk) None else Some(cmdFail) } 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 eb46a81088..903d36b3f8 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 @@ -99,11 +99,11 @@ object ChannelRelay { def translateRelayFailure(originHtlcId: Long, fail: HtlcResult.Fail): CMD_FAIL_HTLC = { fail match { - case f: HtlcResult.RemoteFail => CMD_FAIL_HTLC(originHtlcId, Left(f.fail.reason), commit = true) - case f: HtlcResult.RemoteFailMalformed => CMD_FAIL_HTLC(originHtlcId, Right(createBadOnionFailure(f.fail.onionHash, f.fail.failureCode)), commit = true) - case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure()), commit = true) - case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure()), commit = true) - case f: HtlcResult.DisconnectedBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(TemporaryChannelFailure(Some(f.channelUpdate))), commit = true) + 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) } } @@ -165,7 +165,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, Right(UnknownNextPeer()), commit = true)) + safeSendAndStop(r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) case WrappedPeerReadyResult(r: PeerReadyNotifier.PeerReady) => context.self ! DoRelay relay(Some(r.remoteFeatures), Seq.empty) @@ -201,7 +201,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, Right(UnknownNextPeer()), commit = true) + val cmdFail = CMD_FAIL_HTLC(upstream.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true) Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel) safeSendAndStop(upstream.add.channelId, cmdFail) @@ -260,7 +260,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, Right(failure), delay, commit = true) + CMD_FAIL_HTLC(cmd.id, FailureReason.LocalFailure(failure), delay, commit = true) case None => // We are not the introduction node. CMD_FAIL_MALFORMED_HTLC(cmd.id, failure.onionHash, failure.code, commit = true) @@ -293,9 +293,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, Right(translateLocalError(error.t, error.channelUpdate)), commit = true) + CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(translateLocalError(error.t, error.channelUpdate)), commit = true) } else { - CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), commit = true) + CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true) } walletNodeId_opt match { case Some(walletNodeId) if shouldAttemptOnTheFlyFunding(remoteFeatures_opt, previousFailures) => RelayNeedsFunding(walletNodeId, cmdFail) @@ -326,7 +326,7 @@ class ChannelRelay private(nodeParams: NodeParams, channel.channelUpdate, relayResult match { case _: RelaySuccess => "success" - case RelayFailure(CMD_FAIL_HTLC(_, Right(failureReason), _, _, _)) => failureReason + case RelayFailure(CMD_FAIL_HTLC(_, FailureReason.LocalFailure(failureReason), _, _, _)) => failureReason case other => other }) (channel, relayResult) @@ -373,7 +373,7 @@ class ChannelRelay private(nodeParams: NodeParams, case Some(fail) => RelayFailure(fail) case None if !update.channelFlags.isEnabled => - RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(ChannelDisabled(update.messageFlags, update.channelFlags, Some(update))), commit = true)) + RelayFailure(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(ChannelDisabled(update.messageFlags, update.channelFlags, Some(update))), commit = true)) 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)) @@ -389,11 +389,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, Right(AmountBelowMinimum(r.amountToForward, Some(update))), commit = true)) + Some(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(AmountBelowMinimum(r.amountToForward, Some(update))), commit = true)) } else if (!expiryDeltaOk) { - Some(CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(r.outgoingCltv, Some(update))), commit = true)) + Some(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(IncorrectCltvExpiry(r.outgoingCltv, Some(update))), commit = true)) } else if (!feesOk) { - Some(CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, Some(update))), commit = true)) + Some(CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(FeeInsufficient(r.add.amountMsat, Some(update))), commit = true)) } else { None } 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 ce949050ca..82111f383d 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 @@ -476,7 +476,7 @@ class NodeRelay private(nodeParams: NodeParams, private def rejectHtlc(htlcId: Long, channelId: ByteVector32, amount: MilliSatoshi, failure: Option[FailureMessage] = None): Unit = { val failureMessage = failure.getOrElse(IncorrectOrUnknownPaymentDetails(amount, nodeParams.currentBlockHeight)) - val cmd = CMD_FAIL_HTLC(htlcId, Right(failureMessage), commit = true) + val cmd = CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(failureMessage), commit = true) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) } 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 69f99b7061..adfd1947b7 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 @@ -29,7 +29,6 @@ import fr.acinq.eclair.payment.Monitoring.Metrics import fr.acinq.eclair.wire.protocol.LiquidityAds.PaymentDetails import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, ToMilliSatoshiConversion} -import scodec.bits.ByteVector import scala.concurrent.duration.FiniteDuration @@ -93,22 +92,25 @@ object OnTheFlyFunding { def maxFees(htlcMinimum: MilliSatoshi): MilliSatoshi = htlc.amount - htlcMinimum /** Create commands to fail all upstream HTLCs. */ - def createFailureCommands(failure_opt: Option[Either[ByteVector, FailureMessage]]): Seq[(ByteVector32, CMD_FAIL_HTLC)] = upstream match { + def createFailureCommands(failure_opt: Option[FailureReason]): Seq[(ByteVector32, CMD_FAIL_HTLC)] = upstream match { case _: Upstream.Local => Nil case u: Upstream.Hot.Channel => val failure = htlc.pathKey_opt match { - case Some(_) => Right(InvalidOnionBlinding(Sphinx.hash(u.add.onionRoutingPacket))) - case None => failure_opt.getOrElse(Right(UnknownNextPeer())) + case Some(_) => FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(u.add.onionRoutingPacket))) + case None => failure_opt.getOrElse(FailureReason.LocalFailure(UnknownNextPeer())) } Seq(u.add.channelId -> CMD_FAIL_HTLC(u.add.id, failure, commit = true)) case u: Upstream.Hot.Trampoline => // In the trampoline case, we currently ignore downstream failures: we should add dedicated failures to the // BOLTs to better handle those cases. val failure = failure_opt match { - case Some(f) => f.getOrElse(TemporaryNodeFailure()) - case None => UnknownNextPeer() + case Some(f) => f match { + case _: FailureReason.EncryptedDownstreamFailure => FailureReason.LocalFailure(TemporaryNodeFailure()) + case _: FailureReason.LocalFailure => f + } + case None => FailureReason.LocalFailure(UnknownNextPeer()) } - u.received.map(_.add).map(add => add.channelId -> CMD_FAIL_HTLC(add.id, Right(failure), commit = true)) + u.received.map(_.add).map(add => add.channelId -> CMD_FAIL_HTLC(add.id, failure, 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 9f5f7c639d..48b73bc029 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 @@ -28,7 +28,7 @@ import fr.acinq.eclair.db._ import fr.acinq.eclair.payment.Monitoring.Tags import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket, PaymentFailed, PaymentSent} import fr.acinq.eclair.transactions.DirectedHtlc.outgoing -import fr.acinq.eclair.wire.protocol.{FailureMessage, InvalidOnionBlinding, TemporaryNodeFailure, UpdateAddHtlc} +import fr.acinq.eclair.wire.protocol.{FailureMessage, FailureReason, InvalidOnionBlinding, TemporaryNodeFailure, UpdateAddHtlc} import fr.acinq.eclair.{CustomCommitmentsPlugin, Feature, Features, Logs, MilliSatoshiLong, NodeParams, TimestampMilli} import scala.concurrent.Promise @@ -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, Right(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true) } channel ! cmd } else { @@ -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, Right(TemporaryNodeFailure()), commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), 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 c70c12751e..ed83b4ee35 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 @@ -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, Right(RequiredNodeFeatureMissing()), commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(RequiredNodeFeatureMissing()), 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, Right(InvalidOnionBlinding(badOnion.onionHash)), delay, commit = true) + CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(InvalidOnionBlinding(badOnion.onionHash)), 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, Right(failure), commit = true) + val cmdFail = CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(failure), 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 442ce2f3fc..865505e0b1 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 @@ -36,7 +36,6 @@ import fr.acinq.eclair.router._ import fr.acinq.eclair.wire.protocol._ import java.util.concurrent.TimeUnit -import scala.util.{Failure, Success} /** * Created by PM on 26/08/2016. @@ -166,14 +165,14 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A private def handleRemoteFail(d: WaitingForComplete, fail: UpdateFailHtlc) = { import d._ ((Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { - case success@Success(e) => + case success@Right(e) => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(request.amount, Nil, e))).increment() success - case failure@Failure(_) => - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(request.amount, Nil))).increment() + case failure@Left(e) => + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(request.amount, Nil, e.unwrapped))).increment() failure }) match { - case res@Success(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => + case res@Right(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => // We have discovered some liquidity information with this payment: we update the router accordingly. val stoppedRoute = route.stopAt(nodeId) if (stoppedRoute.hops.length > 1) { @@ -198,39 +197,39 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A res case res => res }) match { - case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == recipient.nodeId => + case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == recipient.nodeId => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(request.amount, route.fullRoute, e)))) - case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if route.finalHop_opt.collect { case h: NodeHop if h.nodeId == nodeId => h }.nonEmpty => + case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if route.finalHop_opt.collect { case h: NodeHop if h.nodeId == nodeId => h }.nonEmpty => // if trampoline node returns an error, we fail the payment immediately log.warning(s"received an error message from trampoline nodeId=$nodeId, failing the payment (failure=$failureMessage)") myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(request.amount, route.fullRoute, e)))) case res if failures.size + 1 >= request.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned val failure = res match { - case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => + case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)") failureMessage match { case failureMessage: Update => handleUpdate(nodeId, failureMessage, d) case _ => } RemoteFailure(request.amount, route.fullRoute, e) - case Failure(t) => - log.warning(s"cannot parse returned error ${fail.reason.toHex} with sharedSecrets=$sharedSecrets: ${t.getMessage}") - UnreadableRemoteFailure(request.amount, route.fullRoute) + 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) } log.warning(s"too many failed attempts, failing the payment") myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ failure))) - case Failure(t) => - log.warning(s"cannot parse returned error: ${t.getMessage}, route=${route.printNodes()}") - val failure = UnreadableRemoteFailure(request.amount, route.fullRoute) + 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) retry(failure, d) - case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) => + case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) => log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") val failure = RemoteFailure(request.amount, route.fullRoute, e) retry(failure, d) - case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => + case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") val failure = RemoteFailure(request.amount, route.fullRoute, e) if (failureMessage.update_opt.forall(update => Announcements.checkSig(update, nodeId))) { @@ -257,7 +256,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore + nodeId) } } - case Success(e@Sphinx.DecryptedFailurePacket(nodeId, _: InvalidOnionBlinding)) => + case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _: InvalidOnionBlinding)) => // there was a failure inside the blinded route we used: we cannot know why it failed, so let's ignore it. log.info(s"received an error coming from nodeId=$nodeId inside the blinded route, retrying with different blinded routes") val failure = RemoteFailure(request.amount, route.fullRoute, e) @@ -270,7 +269,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A router ! RouteRequest(nodeParams.nodeId, recipient, request.routeParams, ignore1, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(request, failures :+ failure, ignore1) } - case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => + case Right(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)") val failure = RemoteFailure(request.amount, route.fullRoute, e) retry(failure, d) @@ -316,7 +315,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A router ! ExcludeChannel(ChannelDesc(ann.channelUpdate.shortChannelId, nodeId, hop.nextNodeId), Some(nodeParams.routerConf.channelExcludeDuration)) } data.recipient.extraEdges - case hint: HopRelayParams.FromHint => + case _: HopRelayParams.FromHint => failure.update_opt match { case Some(update) => log.info("received an update for a routing hint (shortChannelId={} nodeId={} enabled={} update={})", update.shortChannelId, nodeId, update.channelFlags.isEnabled, failure.update_opt) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Recipient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Recipient.scala index 8ba7dba68e..09b46a48e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Recipient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Recipient.scala @@ -78,7 +78,7 @@ case class ClearRecipient(nodeId: PublicKey, ClearRecipient.validateRoute(nodeId, route).map(_ => { val finalPayload = nextTrampolineOnion_opt match { case Some(trampolinePacket) => NodePayload(nodeId, FinalPayload.Standard.createTrampolinePayload(route.amount, totalAmount, expiry, paymentSecret, trampolinePacket)) - case None => NodePayload(nodeId, FinalPayload.Standard.createPayload(route.amount, totalAmount, expiry, paymentSecret, paymentMetadata_opt, customTlvs)) + case None => NodePayload(nodeId, FinalPayload.Standard.createPayload(route.amount, totalAmount, expiry, paymentSecret, paymentMetadata_opt, trampolineOnion_opt = None, customTlvs = customTlvs)) } Recipient.buildPayloads(PaymentPayloads(route.amount, expiry, Seq(finalPayload), None), route.hops) }) 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 8bf7c6d20a..8f9d8d5cfa 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 @@ -30,9 +30,18 @@ object CommandCodecs { // A trailing tlv stream was added in https://github.com/lightning/bolts/pull/1021 which wasn't handled properly by // our previous set of codecs because we didn't prefix failure messages with their length. - private val legacyCmdFailCodec: Codec[CMD_FAIL_HTLC] = + private val cmdFailWithoutLengthCodec: Codec[CMD_FAIL_HTLC] = (("id" | int64) :: - ("reason" | either(bool, varsizebinarydata, provide(TemporaryNodeFailure()).upcast[FailureMessage])) :: + ("reason" | either(bool, varsizebinarydata, provide(TemporaryNodeFailure()).upcast[FailureMessage]).xmap[FailureReason]( + { + case Left(packet) => FailureReason.EncryptedDownstreamFailure(packet) + case Right(f) => FailureReason.LocalFailure(f) + }, + { + case FailureReason.EncryptedDownstreamFailure(packet) => Left(packet) + case FailureReason.LocalFailure(f) => Right(f) + } + )) :: ("delay_opt" | provide(Option.empty[FiniteDuration])) :: ("commit" | provide(false)) :: ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC] @@ -43,9 +52,27 @@ object CommandCodecs { ("commit" | provide(false)) :: ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC] + // We previously supported only two types of HTLC failures, represented by an Either[ByteVector, FailureMessage]. + private val cmdFailEitherCodec: Codec[CMD_FAIL_HTLC] = + (("id" | int64) :: + ("reason" | either(bool8, varsizebinarydata, variableSizeBytes(uint16, failureMessageCodec)).xmap[FailureReason]( + { + case Left(packet) => FailureReason.EncryptedDownstreamFailure(packet) + case Right(f) => FailureReason.LocalFailure(f) + }, + { + case FailureReason.EncryptedDownstreamFailure(packet) => Left(packet) + case FailureReason.LocalFailure(f) => Right(f) + } + )) :: + // 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 cmdFailCodec: Codec[CMD_FAIL_HTLC] = (("id" | int64) :: - ("reason" | either(bool8, varsizebinarydata, variableSizeBytes(uint16, failureMessageCodec))) :: + ("reason" | failureReasonCodec) :: // 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)) :: @@ -60,9 +87,10 @@ object CommandCodecs { val cmdCodec: Codec[HtlcSettlementCommand] = discriminated[HtlcSettlementCommand].by(uint16) // NB: order matters! - .typecase(3, cmdFailCodec) + .typecase(4, cmdFailCodec) + .typecase(3, cmdFailEitherCodec) .typecase(2, cmdFailMalformedCodec) - .typecase(1, legacyCmdFailCodec) + .typecase(1, cmdFailWithoutLengthCodec) .typecase(0, cmdFulfillCodec) } \ No newline at end of file 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 2699a0e482..25aa721788 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 @@ -31,6 +31,17 @@ import scodec.{Attempt, Codec, Err} * Created by fabrice on 14/03/17. */ +// @formatter:off +/** Reason for failing an HTLC, which will be encrypted into a failure onion packet. */ +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 + /** A local failure that should be encrypted for the node that created the payment onion. */ + case class LocalFailure(failure: FailureMessage) extends FailureReason +} +// @formatter:on + sealed trait FailureMessageTlv extends Tlv // @formatter:off @@ -157,6 +168,10 @@ object FailureMessageCodecs { fallback = unknownFailureMessageCodec.upcast[FailureMessage] ) + val failureReasonCodec: Codec[FailureReason] = discriminated[FailureReason].by(uint8) + .typecase(0, varsizebinarydata.as[FailureReason.EncryptedDownstreamFailure]) + .typecase(1, variableSizeBytes(uint16, failureMessageCodec).as[FailureReason.LocalFailure]) + private def failureOnionPayload(payloadAndPadLength: Int): Codec[FailureMessage] = Codec( encoder = f => variableSizeBytes(uint16, failureMessageCodec).encode(f).flatMap(bits => { val payloadLength = bits.bytes.length - 2 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index f028ed9fa8..6e22a25948 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -431,6 +431,7 @@ object PaymentOnion { val paymentSecret = records.get[PaymentData].map(_.secret).orElse(records.get[KeySend].map(_.paymentPreimage)).get val paymentPreimage = records.get[KeySend].map(_.paymentPreimage) val paymentMetadata = records.get[PaymentMetadata].map(_.data) + val isTrampoline = records.get[TrampolineOnion].nonEmpty } object Standard { @@ -442,12 +443,13 @@ object PaymentOnion { Right(Standard(records)) } - def createPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector] = None, customTlvs: Set[GenericTlv] = Set.empty): Standard = { + def createPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector] = None, trampolineOnion_opt: Option[OnionRoutingPacket] = None, customTlvs: Set[GenericTlv] = Set.empty): Standard = { val tlvs: Set[OnionPaymentPayloadTlv] = Set( Some(AmountToForward(amount)), Some(OutgoingCltv(expiry)), Some(PaymentData(paymentSecret, totalAmount)), - paymentMetadata.map(m => PaymentMetadata(m)) + paymentMetadata.map(m => PaymentMetadata(m)), + trampolineOnion_opt.map(o => TrampolineOnion(o)), ).flatten Standard(TlvStream(tlvs, customTlvs)) } 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 8aeb636b0e..5fd196fe17 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 @@ -23,7 +23,7 @@ import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UnknownNextPeer, UpdateAddHtlc} +import fr.acinq.eclair.wire.protocol.{CommitSig, FailureReason, RevokeAndAck, UnknownNextPeer, UpdateAddHtlc} import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector @@ -248,7 +248,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) == None) - alice ! CMD_FAIL_HTLC(1, Right(UnknownNextPeer()), replyTo_opt = Some(probe.ref)) + alice ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(UnknownNextPeer()), 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) @@ -378,7 +378,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel } assert(!rcp3.isDone) - bob ! CMD_FAIL_HTLC(bobPendingHtlc.htlc.id, Right(UnknownNextPeer()), replyTo_opt = Some(probe.ref)) + bob ! CMD_FAIL_HTLC(bobPendingHtlc.htlc.id, FailureReason.LocalFailure(UnknownNextPeer()), 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 94b796cd8e..62180a9cd6 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 @@ -27,7 +27,7 @@ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.keymanager.LocalChannelKeyManager import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx -import fr.acinq.eclair.wire.protocol.{IncorrectOrUnknownPaymentDetails, UpdateAddHtlc, UpdateFailHtlc} +import fr.acinq.eclair.wire.protocol.{FailureReason, IncorrectOrUnknownPaymentDetails, UpdateAddHtlc, UpdateFailHtlc} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -199,7 +199,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc4.availableBalanceForSend == b) assert(bc4.availableBalanceForReceive == a - p - htlcOutputFee) - val cmdFail = CMD_FAIL_HTLC(0, Right(IncorrectOrUnknownPaymentDetails(p, BlockHeight(42)))) + val cmdFail = CMD_FAIL_HTLC(0, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(p, BlockHeight(42)))) val Right((bc5, fail: UpdateFailHtlc)) = bc4.sendFail(cmdFail, bob.underlyingActor.nodeParams.privateKey) assert(bc5.availableBalanceForSend == b) assert(bc5.availableBalanceForReceive == a - p - htlcOutputFee) // a's balance won't return to previous before she acknowledges the fail @@ -322,7 +322,7 @@ 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, Right(IncorrectOrUnknownPaymentDetails(p2, BlockHeight(42)))) + val cmdFail2 = CMD_FAIL_HTLC(1, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(p2, BlockHeight(42)))) val Right((bc9, fail2: UpdateFailHtlc)) = bc8.sendFail(cmdFail2, bob.underlyingActor.nodeParams.privateKey) 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 53b9953633..278f45290b 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 @@ -429,7 +429,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, Right(TemporaryNodeFailure())) + s ! CMD_FAIL_HTLC(id, FailureReason.LocalFailure(TemporaryNodeFailure())) 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 0ae008b0cf..a8fd0195aa 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 @@ -191,7 +191,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, Left(randomBytes32())) + case FailHtlc => CMD_FAIL_HTLC(add.id, FailureReason.EncryptedDownstreamFailure(randomBytes(252))) } 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 4aa5046f62..9e1ff88880 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 @@ -42,7 +42,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits._ @@ -1527,7 +1527,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, Right(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure())) val fail = bob2alice.expectMsgType[UpdateFailHtlc] bob2alice.forward(alice) bob ! CMD_SIGN() @@ -1811,7 +1811,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val cmd = CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure())) + val cmd = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure())) val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, cmd, htlc) assert(fail.id == htlc.id) bob ! cmd @@ -1841,7 +1841,7 @@ 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, Right(PermanentChannelFailure()), delay_opt = Some(50 millis)) + 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) assert(fail.id == htlc.id) bob ! cmd @@ -1854,7 +1854,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FAIL_HTLC(42, Right(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) @@ -1874,7 +1874,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, Right(TemporaryNodeFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(TemporaryNodeFailure()), replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), htlc.id))) } @@ -1884,7 +1884,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val c = CMD_FAIL_HTLC(42, Right(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), 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) @@ -1938,7 +1938,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, Right(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(PermanentChannelFailure())) val fail = bob2alice.expectMsgType[UpdateFailHtlc] // actual test begins @@ -2045,7 +2045,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, Left(ByteVector.fill(561)(42))) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.EncryptedDownstreamFailure(ByteVector.fill(561)(42))) 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 b0115548c5..f3c7cdeaeb 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 @@ -657,7 +657,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, Right(IncorrectOrUnknownPaymentDetails(0 msat, BlockHeight(0)))) + bob ! CMD_FAIL_HTLC(htlc.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(0 msat, BlockHeight(0)))) 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 c7a0258441..8e49654cc3 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 @@ -32,7 +32,7 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.send.SpontaneousRecipient import fr.acinq.eclair.transactions.Transactions.ClaimLocalAnchorOutputTx -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -243,7 +243,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, Right(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PermanentChannelFailure())) val fail = bob2alice.expectMsgType[UpdateFailHtlc] awaitCond(bob.stateData == initialState .modify(_.commitments.changes.localChanges.proposed).using(_ :+ fail) @@ -254,7 +254,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, Right(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) bob ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) @@ -264,7 +264,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, Right(PermanentChannelFailure()), replyTo_opt = Some(sender.ref)) + val c = CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PermanentChannelFailure()), 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) @@ -503,7 +503,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv RevokeAndAck (forward UpdateFailHtlc)") { f => import f._ - bob ! CMD_FAIL_HTLC(1, Right(PermanentChannelFailure())) + bob ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(PermanentChannelFailure())) 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 3ba3890df2..05bdf7a4ff 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 @@ -229,19 +229,19 @@ class SphinxSpec extends AnyFunSuite { val packet1 = FailurePacket.create(sharedSecrets.head, expected.failureMessage) assert(packet1.length == 292) - val Success(decrypted1) = FailurePacket.decrypt(packet1, Seq(0).map(i => (sharedSecrets(i), publicKeys(i)))) + val Right(decrypted1) = FailurePacket.decrypt(packet1, Seq(0).map(i => (sharedSecrets(i), publicKeys(i)))) assert(decrypted1 == expected) val packet2 = FailurePacket.wrap(packet1, sharedSecrets(1)) assert(packet2.length == 292) - val Success(decrypted2) = FailurePacket.decrypt(packet2, Seq(1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) + val Right(decrypted2) = FailurePacket.decrypt(packet2, Seq(1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) assert(decrypted2 == expected) val packet3 = FailurePacket.wrap(packet2, sharedSecrets(2)) assert(packet3.length == 292) - val Success(decrypted3) = FailurePacket.decrypt(packet3, Seq(2, 1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) + val Right(decrypted3) = FailurePacket.decrypt(packet3, Seq(2, 1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) assert(decrypted3 == expected) } @@ -258,7 +258,7 @@ class SphinxSpec extends AnyFunSuite { sharedSecrets(1)), sharedSecrets(2)) - assert(FailurePacket.decrypt(packet, Seq(0, 2, 1).map(i => (sharedSecrets(i), publicKeys(i)))).isFailure) + assert(FailurePacket.decrypt(packet, Seq(0, 2, 1).map(i => (sharedSecrets(i), publicKeys(i)))).isLeft) } test("last node replies with a short failure message (old reference test vector)") { @@ -286,7 +286,7 @@ class SphinxSpec extends AnyFunSuite { assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") // origin parses error packet and can see that it comes from node #4 - val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, sharedSecrets) assert(pubkey == publicKeys(4)) assert(failure == TemporaryNodeFailure()) } @@ -318,7 +318,7 @@ class SphinxSpec extends AnyFunSuite { assert(error4 == hex"2dd2f49c1f5af0fcad371d96e8cddbdcd5096dc309c1d4e110f955926506b3c03b44c192896f45610741c85ed4074212537e0c118d472ff3a559ae244acd9d783c65977765c5d4e00b723d00f12475aafaafff7b31c1be5a589e6e25f8da2959107206dd42bbcb43438129ce6cce2b6b4ae63edc76b876136ca5ea6cd1c6a04ca86eca143d15e53ccdc9e23953e49dc2f87bb11e5238cd6536e57387225b8fff3bf5f3e686fd08458ffe0211b87d64770db9353500af9b122828a006da754cf979738b4374e146ea79dd93656170b89c98c5f2299d6e9c0410c826c721950c780486cd6d5b7130380d7eaff994a8503a8fef3270ce94889fe996da66ed121741987010f785494415ca991b2e8b39ef2df6bde98efd2aec7d251b2772485194c8368451ad49c2354f9d30d95367bde316fec6cbdddc7dc0d25e99d3075e13d3de0822669861dafcd29de74eac48b64411987285491f98d78584d0c2a163b7221ea796f9e8671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7cd13a7fe3f72a409bc3bdabfe0ba47a6d861e21f64d2fac706dab18b3e546df4") // origin parses error packet and can see that it comes from node #4 - val Success(DecryptedFailurePacket(pubkey, parsedFailure)) = FailurePacket.decrypt(error4, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, parsedFailure)) = FailurePacket.decrypt(error4, sharedSecrets) assert(pubkey == publicKeys(4)) assert(parsedFailure == failure) } @@ -346,7 +346,7 @@ class SphinxSpec extends AnyFunSuite { assert(error4 == hex"751c187d145e5498306824f193c6bf9ed4a974fa85b3cc5d32d549ce494c1e7b3a06a19f8a9145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12dc942b5cf1db059d3e73d63967e464b5d5cfd4052de195387de93535e88a2e618e15a7c521d67ce2cc836c49118f205c99f18570504504221e337a29e2716fb28671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7cd13a7fe3f72a409bc3bdabfe0ba47a6d861e21f64d2fac706dab18b3e546df4") // origin parses error packet and can see that it comes from node #4 - val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, sharedSecrets) assert(pubkey == publicKeys(4)) assert(failure == TemporaryNodeFailure()) } @@ -366,7 +366,7 @@ class SphinxSpec extends AnyFunSuite { val error2 = FailurePacket.wrap(error1, sharedSecret0) // origin parses error packet and can see that it comes from node #2 - val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, sharedSecrets) assert(pubkey == publicKeys(2)) assert(failure == InvalidRealm()) } @@ -387,7 +387,7 @@ class SphinxSpec extends AnyFunSuite { assert(error2 == hex"c843486107187673b4586f5cdaad43ad84fbac03b39df51bbf9169b2bd682b409a855b2feb0545705f12eba9dbaecee84e328a9c2e4c3086bb1d0909d1f2e4f8a0e9c6be9541e94a849a0887756b984031dcb74d11c20d437a55daf3ee4109dea68ad74f9b742e7571d5e4d1b2ea4f7094787cf361b448a22a547ea85b833aae20f3ba79fb41c6636414c2092d41dd5328e2c1a1c754cb1f0d297628219f91fe946169f593ce7fce79103945d4d24adce46c083ab24757870356af55fcd3d22b9cfd83c45d409eb3081b218448d5dca3a201cf89ac88c9b66049d7c262b32081d3aba2098ea853bfa173ec23aa9253e083dfa881ef487b76780435c1b9f8a1d794557f0ac91d261d280bfb8513ad0c4dab0d7152eb9ee36ae63b8d384613684326d8735dc559f31cecb21b1d55bbcf7a281127adbedd0210b243325fd291cb82d443beec8f4b96aaee4b1a619724d7456b756d391e8fd3256d2b0766e39a435eb4d6d144c7fca1c73105710266e31120565444dfd6e9099e44d73a0f28419809577a267bbbc6671f723669d00c35c8e60fad88d89d4a7477a0c30f9839485197ed76338330f2ca00cf0e31c59da4eeebef977f429ad2c61acac35939866dac5b1df1c3c487ebaf961340c0c1dbc4bedebde7ee0633c3f480b7df265a3d90e78a4bcb9497f4228169fadb647e77afe6f43aa129286bb21767f6e75ac5c092473f99f2cf8b4e191f300c70b210e077a0385d483971bc0c66f5c119c0731a8753793ad12703d9cc5153eb1c8f25b71ee88a8d1d4433aa8f8277366c82111dbebfe0f548411588d54c3606742330d3d84a2f107df98d60995297de11672f6300b11444a04e252d69d8187772798afc6a9cd8b245a5ebd51bf0659f18c57daf1d1f724d2f15d524ab6902fb17a8fa6cee8e01df67735eac34bb0efc183dcb8d2a7cb401bd786c32a17f14c9d9ffc02b4f58c4ebab898a78b4913647d4cb5bafe6f7f27b5a256d1635c10f0ca71796610068c090c270c20bb18ec9d205e640d7655bdf5c9aeae20d7f9426eade0733c19d0aa577caf31f9d5be0a99ed0c509e84ccb555389ca69f09c3e66694a4ea2785f8d839d7dfff08b2c21aff89a023161cb1ebdd1e7a46d6380c0ddbc88eb3526e624fadcd222ecaa09566c2678158f933f03623299fec134a880d39a9d82ba2b29211e7787b3f32d478df856389a02cb68b66fc0dfc0b52353e7360f31e5457a6a9dd34512e912afeb5a92f3cbd3883b62c37e3ba5e4e8b688033150103c810740d130a5597c8a4a16311f50cfb3a919aac1e0a1096f20a14a536c55068ad38f40e62fc6f178b2fee67ca2cbd8afa29ef6c89b217aee02419ca26d59b604521a55e37c0a5a693fbc3ebcba23cd62479ddf62e5521847a2b4ac5e7686ef662c29cf8a8983660530942ee9a6c53b55e08af0b43467989693cefe6267fd524435152c01c9b93aebdec6146366a94162f99ac4c7157c15b988") // origin parses error packet and can see that it comes from node #2 - val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, sharedSecrets) + val Right(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, sharedSecrets) assert(pubkey == publicKeys(2)) assert(failure == InvalidRealm()) } 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 705252566f..7f957da50b 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 @@ -24,7 +24,7 @@ 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.wire.internal.CommandCodecs.cmdCodec -import fr.acinq.eclair.wire.protocol.{FailureMessageCodecs, UnknownNextPeer} +import fr.acinq.eclair.wire.protocol.{FailureMessageCodecs, FailureReason, UnknownNextPeer} import org.scalatest.funsuite.AnyFunSuite import scala.util.Random @@ -53,8 +53,8 @@ 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, Left(randomBytes32())) - val msg3 = CMD_FAIL_HTLC(3, Left(randomBytes32())) + val msg2 = CMD_FAIL_HTLC(2, FailureReason.EncryptedDownstreamFailure(randomBytes32())) + val msg3 = CMD_FAIL_HTLC(3, FailureReason.EncryptedDownstreamFailure(randomBytes32())) val msg4 = CMD_FAIL_MALFORMED_HTLC(4, randomBytes32(), FailureMessageCodecs.BADONION) assert(db.listSettlementCommands(channelId1).toSet == Set.empty) @@ -135,7 +135,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), Right(UnknownNextPeer())) + case 1 => CMD_FAIL_HTLC(Random.nextLong(100_000), FailureReason.LocalFailure(UnknownNextPeer())) } } 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 e3bae392a9..ab92cb1bf0 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 @@ -199,7 +199,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike 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))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) eventListener.expectNoMessage(100 milliseconds) @@ -374,7 +374,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike 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))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(incoming.invoice.isExpired() && incoming.status == IncomingPaymentStatus.Expired) } @@ -389,7 +389,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike 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))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + 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 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))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } @@ -418,7 +418,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike 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))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } @@ -432,7 +432,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike 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))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } @@ -446,7 +446,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike 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))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } @@ -461,7 +461,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike 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))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } @@ -477,7 +477,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(receivePayment.paymentHash == invoice.paymentHash) receivePayment.replyTo ! GetIncomingPaymentActor.RejectPayment("non blinded payment") val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } @@ -493,7 +493,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike 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))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).isEmpty) } @@ -537,7 +537,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(payment.payload.pathId == pathId) payment.replyTo ! GetIncomingPaymentActor.RejectPayment("internal error") val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight))) } test("PaymentHandler should reject incoming blinded payment with unexpected expiry") { f => @@ -560,7 +560,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val payment = IncomingBlindedPayment(MinimalBolt12Invoice(invoice.records), preimage, PaymentType.Blinded, TimestampMilli.now(), IncomingPaymentStatus.Pending) receivePayment.replyTo ! GetIncomingPaymentActor.ProcessPayment(payment) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).isEmpty) } @@ -587,8 +587,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, Right(PaymentTimeout()), commit = true)), - Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(1, Right(PaymentTimeout()), commit = true)) + 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)) )) awaitCond({ f.sender.send(handler, GetPendingPayments) @@ -597,7 +597,7 @@ 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, Right(PaymentTimeout()), commit = true))) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PaymentTimeout()), commit = true))) // The payment should still be pending in DB. val Some(incomingPayment) = nodeParams.db.payments.getIncomingPayment(pr1.paymentHash) @@ -621,7 +621,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) f.register.expectMsgAllOf( - Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), + Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), 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)) ) @@ -684,7 +684,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike 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, Right(PaymentTimeout()), commit = true))) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, FailureReason.LocalFailure(PaymentTimeout()), commit = true))) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty @@ -770,7 +770,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0, None) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, payload)) - f.register.expectMsg(Register.Forward(null, add.channelId, CMD_FAIL_HTLC(add.id, Right(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)), commit = true))) assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) } @@ -785,7 +785,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, paymentSecret, None))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } test("PaymentHandler should reject incoming multi-part payment if the invoice doesn't exist") { f => @@ -799,7 +799,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345")))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } test("PaymentHandler should fail fulfilling incoming payments if the invoice doesn't exist") { f => @@ -816,7 +816,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithoutMpp, fulfill) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) - assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } } 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 d85030a989..1d8c5bdfc7 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 @@ -385,7 +385,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) + UnreadableRemoteFailure(finalAmount, Nil, randomBytes(292)) ) val extraEdges1 = Seq( ExtraEdge(a, b, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12), 1 msat, None), @@ -421,14 +421,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)))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, randomBytes(292))))) 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)))) + val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, failedRoute2.hops, randomBytes(292))))) assert(result.failures.length >= 3) assert(result.failures.contains(LocalFailure(finalAmount, Nil, RetryExhausted))) @@ -517,7 +517,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)))) + childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, randomBytes(292))))) router.expectMsgType[RouteRequest] val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout()))))) @@ -535,7 +535,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)))) + childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.amount, failedRoute.fullRoute, randomBytes(292))))) 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 3c1b4a5f98..8b2bf22c63 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 @@ -44,6 +44,7 @@ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ +import org.scalatest.Inside.inside import scodec.bits.ByteVector import java.util.UUID @@ -338,7 +339,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(ignore1.nodes.isEmpty) register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes32())))) // unparsable message + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes(292))))) // unparsable message // then the payment lifecycle will ask for a new route excluding all intermediate nodes routerForwarder.expectMsg(defaultRouteRequest(a, cfg).copy(ignore = Ignore(Set(c), Set.empty))) @@ -350,10 +351,17 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(ignore2.nodes == Set(c)) // and reply a 2nd time with an unparsable failure register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd2)) - sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)))) // unparsable message + sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes(292))))) // unparsable message // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures == UnreadableRemoteFailure(route.amount, route.hops) :: UnreadableRemoteFailure(route.amount, route.hops) :: Nil) + inside(sender.expectMsgType[PaymentFailed]) { e => + assert(e.failures.length == 2) + e.failures.foreach(f => { + assert(f.isInstanceOf[UnreadableRemoteFailure]) + assert(f.amount == route.amount) + assert(f.route == route.hops) + }) + } awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) // after last attempt the payment is failed val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -881,11 +889,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { (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), Set.empty, Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: 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), 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), Set(c), Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: blindedHop_de :: Nil), Set(c), Set.empty), + (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), ) for ((failure, expectedNodes, expectedChannels) <- testCases) { 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 93cc4e5d35..0ffb7c60c2 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 @@ -35,7 +35,6 @@ import fr.acinq.eclair.router.Router.{NodeHop, Route} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer, PaymentInfo} -import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData} import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, OutgoingBlindedPerHopPayload} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampMilli, TimestampSecondLong, UInt64, nodeFee, randomBytes32, randomKey} @@ -44,7 +43,6 @@ import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} import scala.concurrent.duration._ -import scala.util.Success /** * Created by PM on 31/05/2016. @@ -312,7 +310,12 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { 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) assert(add_e2 == add_e) - assert(payload_e == FinalPayload.Standard(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, finalAmount), OnionPaymentPayloadTlv.PaymentMetadata(hex"010203")))) + assert(payload_e.isInstanceOf[FinalPayload.Standard]) + assert(payload_e.amount == finalAmount) + assert(payload_e.expiry == finalExpiry) + assert(payload_e.asInstanceOf[FinalPayload.Standard].paymentSecret == paymentSecret) + assert(payload_e.asInstanceOf[FinalPayload.Standard].paymentMetadata.contains(hex"010203")) + assert(payload_e.asInstanceOf[FinalPayload.Standard].isTrampoline) } test("build outgoing trampoline payment with non-trampoline recipient") { @@ -355,7 +358,12 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { 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) assert(add_e2 == add_e) - assert(payload_e == FinalPayload.Standard(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(invoice.paymentSecret, finalAmount), OnionPaymentPayloadTlv.PaymentMetadata(hex"010203")))) + assert(payload_e.isInstanceOf[FinalPayload.Standard]) + assert(payload_e.amount == finalAmount) + assert(payload_e.expiry == finalExpiry) + assert(payload_e.asInstanceOf[FinalPayload.Standard].paymentSecret == invoice.paymentSecret) + assert(payload_e.asInstanceOf[FinalPayload.Standard].paymentMetadata.contains(hex"010203")) + assert(!payload_e.asInstanceOf[FinalPayload.Standard].isTrampoline) } test("build outgoing trampoline payment with non-trampoline recipient and dummy trampoline packet") { @@ -412,7 +420,12 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { 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) assert(add_e2 == add_e) - assert(payload_e == FinalPayload.Standard(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(invoice.paymentSecret, finalAmount), OnionPaymentPayloadTlv.PaymentMetadata(hex"010203")))) + assert(payload_e.isInstanceOf[FinalPayload.Standard]) + assert(payload_e.amount == finalAmount) + assert(payload_e.expiry == finalExpiry) + assert(payload_e.asInstanceOf[FinalPayload.Standard].paymentSecret == invoice.paymentSecret) + assert(payload_e.asInstanceOf[FinalPayload.Standard].paymentMetadata.contains(hex"010203")) + assert(!payload_e.asInstanceOf[FinalPayload.Standard].isTrampoline) } test("fail to build outgoing payment with invalid route") { @@ -649,15 +662,15 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // 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, Right(failure)), add_e) + val Right(fail_e: UpdateFailHtlc) = buildHtlcFailure(priv_e.privateKey, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(failure)), add_e) assert(fail_e.id == add_e.id) - val Right(fail_d: UpdateFailHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, Left(fail_e.reason)), add_d) + val Right(fail_d: UpdateFailHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, FailureReason.EncryptedDownstreamFailure(fail_e.reason)), add_d) assert(fail_d.id == add_d.id) - val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, Left(fail_d.reason)), add_c) + val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, FailureReason.EncryptedDownstreamFailure(fail_d.reason)), add_c) assert(fail_c.id == add_c.id) - val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, Left(fail_c.reason)), add_b) + val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason)), add_b) assert(fail_b.id == add_b.id) - val Success(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, payment.sharedSecrets) + val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, payment.sharedSecrets) assert(failingNode == e) assert(decryptedFailure == failure) } @@ -679,21 +692,21 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { 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, Right(TemporaryNodeFailure())), add_e) + val Right(fail_e: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_e.privateKey, CMD_FAIL_HTLC(add_e.id, FailureReason.LocalFailure(TemporaryNodeFailure())), 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, Right(UnknownNextPeer())), add_d) + val Right(fail_d: UpdateFailMalformedHtlc) = buildHtlcFailure(priv_d.privateKey, CMD_FAIL_HTLC(add_d.id, FailureReason.LocalFailure(UnknownNextPeer())), 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, Right(failure)), add_c) + val Right(fail_c: UpdateFailHtlc) = buildHtlcFailure(priv_c.privateKey, CMD_FAIL_HTLC(add_c.id, FailureReason.LocalFailure(failure)), add_c) assert(fail_c.id == add_c.id) - val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, Left(fail_c.reason)), add_b) + val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, CMD_FAIL_HTLC(add_b.id, FailureReason.EncryptedDownstreamFailure(fail_c.reason)), add_b) assert(fail_b.id == add_b.id) - val Success(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, payment.sharedSecrets) + val Right(Sphinx.DecryptedFailurePacket(failingNode, decryptedFailure)) = Sphinx.FailurePacket.decrypt(fail_b.reason, payment.sharedSecrets) 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 480bcb9db7..f7e462089b 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, Right(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), 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, Right(TemporaryNodeFailure()), commit = true), - CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(4, FailureReason.LocalFailure(TemporaryNodeFailure()), 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, Right(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), 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, Right(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), CMD_FULFILL_HTLC(3, preimage, commit = true), CMD_FULFILL_HTLC(5, preimage, commit = true), - CMD_FAIL_HTLC(7, Right(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), 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, Right(TemporaryNodeFailure()), commit = true), - CMD_FAIL_HTLC(3, Right(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), + CMD_FAIL_HTLC(3, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true), CMD_FULFILL_HTLC(4, preimage, commit = true), - CMD_FAIL_HTLC(9, Right(TemporaryNodeFailure()), commit = true) + CMD_FAIL_HTLC(9, FailureReason.LocalFailure(TemporaryNodeFailure()), 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, Right(TemporaryNodeFailure()), commit = true)) - channel_upstream_2.expectMsg(CMD_FAIL_HTLC(7, Right(TemporaryNodeFailure()), commit = true)) + 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.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, Right(PermanentChannelFailure()))) + nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FAIL_HTLC(4, FailureReason.LocalFailure(PermanentChannelFailure()))) 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, Right(TemporaryNodeFailure()), commit = true)) + case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), 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, Right(TemporaryNodeFailure()), commit = true)) + case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), 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, Right(TemporaryNodeFailure()), commit = true)) + channel.expectMsg(CMD_FAIL_HTLC(1L, FailureReason.LocalFailure(TemporaryNodeFailure()), 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 = Left(ByteVector.empty), replyTo_opt = None) - val cmd2 = CMD_FAIL_HTLC(id = 1L, reason = Left(ByteVector.empty), replyTo_opt = None) + 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 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 883d4c23f4..f988170b42 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 @@ -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, Right(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) } } @@ -348,7 +348,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, Right(TemporaryChannelFailure(Some(u1.channelUpdate))), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(TemporaryChannelFailure(Some(u1.channelUpdate))), commit = true)) } test("fail to relay when we have no channel_update for the next channel") { f => @@ -359,7 +359,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, Right(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) } test("fail to relay when register returns an error") { f => @@ -375,7 +375,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, Right(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) } test("fail to relay when the channel is advertised as unusable (down)") { f => @@ -390,7 +390,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, Right(UnknownNextPeer()), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) } test("fail to relay when channel is disabled") { f => @@ -403,7 +403,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, Right(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))), commit = true)) } test("fail to relay when amount is below minimum") { f => @@ -416,7 +416,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, Right(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))), commit = true)) } test("fail to relay blinded payment") { f => @@ -436,7 +436,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a assert(cmd.message.isInstanceOf[CMD_FAIL_HTLC]) val fail = cmd.message.asInstanceOf[CMD_FAIL_HTLC] assert(fail.id == r.add.id) - assert(fail.reason == Right(InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket)))) + assert(fail.reason == FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket)))) assert(fail.delay_opt.nonEmpty) } else { assert(cmd.message.isInstanceOf[CMD_FAIL_MALFORMED_HTLC]) @@ -464,7 +464,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a peerReadyManager.expectMessageType[PeerReadyManager.Register].replyTo ! PeerReadyManager.Registered(outgoingNodeId, otherAttempts = 0) assert(switchboard.expectMessageType[Switchboard.GetPeerInfo].remoteNodeId == outgoingNodeId) val fail = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fail.message.reason.contains(InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket)))) + assert(fail.message.reason == FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket)))) cleanUpWakeUpActors(peerReadyManager, switchboard) } @@ -492,7 +492,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, Right(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))), commit = true)) } test("fail to relay when fee is insufficient") { f => @@ -505,7 +505,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, Right(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))), commit = true)) } test("relay that would fail (fee insufficient) with a recent channel update but succeed with the previous update") { f => @@ -536,7 +536,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, Right(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))), commit = true)) } test("fail to relay when there is a local error") { f => @@ -565,7 +565,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, Right(testCase.failure), commit = true)) + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, FailureReason.LocalFailure(testCase.failure), commit = true)) } } @@ -611,7 +611,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, Right(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))), commit = true)) } { // higher amount payment (have to increased incoming htlc amount for fees to be sufficient) @@ -646,7 +646,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, Right(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))), commit = true)) } } @@ -662,11 +662,11 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a case class TestCase(result: HtlcResult, cmd: channel.HtlcSettlementCommand) val testCases = Seq( - TestCase(HtlcResult.RemoteFail(UpdateFailHtlc(channelId1, downstream_htlc.id, hex"deadbeef")), CMD_FAIL_HTLC(r.add.id, Left(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, Right(InvalidOnionHmac(ByteVector32.One)), commit = true)), - TestCase(HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId1, downstream_htlc)), CMD_FAIL_HTLC(r.add.id, Right(PermanentChannelFailure()), commit = true)), - TestCase(HtlcResult.DisconnectedBeforeSigned(u_disabled.channelUpdate), CMD_FAIL_HTLC(r.add.id, Right(TemporaryChannelFailure(Some(u_disabled.channelUpdate))), commit = true)), - TestCase(HtlcResult.ChannelFailureBeforeSigned, CMD_FAIL_HTLC(r.add.id, Right(PermanentChannelFailure()), commit = true)) + 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)) ) testCases.foreach { testCase => @@ -707,7 +707,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a assert(cmd.message.isInstanceOf[CMD_FAIL_HTLC]) val fail = cmd.message.asInstanceOf[CMD_FAIL_HTLC] assert(fail.id == r.add.id) - assert(fail.reason == Right(InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket)))) + assert(fail.reason == FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(r.add.onionRoutingPacket)))) assert(fail.delay_opt.nonEmpty) } else { assert(cmd.message.isInstanceOf[CMD_FAIL_MALFORMED_HTLC]) 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 c534b641e0..18a9e45f8c 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 @@ -229,7 +229,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.dropRight(1).foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]](30 seconds) assert(fwd.channelId == p.add.channelId) - val failure = Right(PaymentTimeout()) + val failure = FailureReason.LocalFailure(PaymentTimeout()) assert(fwd.message == CMD_FAIL_HTLC(p.add.id, failure, commit = true)) } @@ -254,8 +254,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // the extra payment will be rejected val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == extra.add.channelId) - val failure = IncorrectOrUnknownPaymentDetails(extra.add.amountMsat, nodeParams.currentBlockHeight) - assert(fwd.message == CMD_FAIL_HTLC(extra.add.id, Right(failure), commit = true)) + val failure = FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(extra.add.amountMsat, nodeParams.currentBlockHeight)) + assert(fwd.message == CMD_FAIL_HTLC(extra.add.id, failure, commit = true)) register.expectNoMessage(100 millis) } @@ -282,8 +282,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd1 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd1.channelId == i1.add.channelId) - val failure1 = IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight) - assert(fwd1.message == CMD_FAIL_HTLC(i1.add.id, Right(failure1), commit = true)) + val failure1 = FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)) + assert(fwd1.message == CMD_FAIL_HTLC(i1.add.id, failure1, commit = true)) // Receive new HTLC with different details, but for the same payment hash. val i2 = IncomingPaymentPacket.RelayToTrampolinePacket( @@ -295,8 +295,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd2 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd1.channelId == i1.add.channelId) - val failure2 = IncorrectOrUnknownPaymentDetails(1500 msat, nodeParams.currentBlockHeight) - assert(fwd2.message == CMD_FAIL_HTLC(i2.add.id, Right(failure2), commit = true)) + val failure2 = FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1500 msat, nodeParams.currentBlockHeight)) + assert(fwd2.message == CMD_FAIL_HTLC(i2.add.id, failure2, commit = true)) register.expectNoMessage(100 millis) } @@ -312,7 +312,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), commit = true)) register.expectNoMessage(100 millis) } @@ -328,7 +328,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), commit = true)) register.expectNoMessage(100 millis) } @@ -349,7 +349,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, Right(TrampolineExpiryTooSoon()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineExpiryTooSoon()), commit = true)) } register.expectNoMessage(100 millis) @@ -398,7 +398,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) register.expectNoMessage(100 millis) } @@ -416,7 +416,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, Right(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) } register.expectNoMessage(100 millis) @@ -431,7 +431,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == p.add.channelId) - assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(InvalidOnionPayload(UInt64(2), 0)), commit = true)) register.expectNoMessage(100 millis) } @@ -449,7 +449,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, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(InvalidOnionPayload(UInt64(2), 0)), commit = true)) } register.expectNoMessage(100 millis) @@ -471,7 +471,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, Right(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) } register.expectNoMessage(100 millis) @@ -496,7 +496,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, Right(TemporaryNodeFailure()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) } register.expectNoMessage(100 millis) @@ -519,7 +519,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, Right(TrampolineFeeInsufficient()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TrampolineFeeInsufficient()), commit = true)) } register.expectNoMessage(100 millis) @@ -535,13 +535,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) :: Nil + val failures = RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(outgoingAmount, Nil, ByteVector.empty) :: 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, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(FinalIncorrectHtlcAmount(42 msat)), commit = true)) } register.expectNoMessage(100 millis) @@ -702,7 +702,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, Right(TemporaryNodeFailure()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(TemporaryNodeFailure()), commit = true)) } parent.expectMessageType[NodeRelayer.RelayComplete] } @@ -923,7 +923,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, Right(UnknownNextPeer()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) } } @@ -993,7 +993,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, Right(UnknownNextPeer()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) } } @@ -1054,7 +1054,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, Right(UnknownNextPeer()), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, FailureReason.LocalFailure(UnknownNextPeer()), commit = true)) } } 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 a20be3039f..6fb20eaa13 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 @@ -230,7 +230,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 == Left(fail1.reason)) + assert(fwd1.message.reason == FailureReason.EncryptedDownstreamFailure(fail1.reason)) register.expectNoMessage(100 millis) val fail2 = WillFailHtlc(willAdd2.id, paymentHash, randomBytes(50)) @@ -238,14 +238,14 @@ 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 == Right(InvalidOnionBlinding(Sphinx.hash(upstream2.add.onionRoutingPacket)))) + assert(fwd2.message.reason == FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(upstream2.add.onionRoutingPacket)))) val fail3 = WillFailMalformedHtlc(willAdd3.id, paymentHash, randomBytes32(), InvalidOnionHmac(randomBytes32()).code) peerConnection.send(peer, fail3) val fwd3 = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd3.channelId == upstream3.add.channelId) assert(fwd3.message.id == upstream3.add.id) - assert(fwd3.message.reason == Right(InvalidOnionHmac(fail3.onionHash))) + assert(fwd3.message.reason == FailureReason.LocalFailure(InvalidOnionHmac(fail3.onionHash))) val fail4 = WillFailHtlc(willAdd4.id, paymentHash, randomBytes(75)) peerConnection.send(peer, fail4) @@ -253,7 +253,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == add.channelId) assert(fwd.message.id == add.id) - assert(fwd.message.reason == Right(TemporaryNodeFailure())) + assert(fwd.message.reason == FailureReason.LocalFailure(TemporaryNodeFailure())) }) val fail5 = WillFailHtlc(willAdd5.id, paymentHash, randomBytes(75)) @@ -262,7 +262,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == add.channelId) assert(fwd.message.id == add.id) - assert(fwd.message.reason == Right(TemporaryNodeFailure())) + assert(fwd.message.reason == FailureReason.LocalFailure(TemporaryNodeFailure())) }) } @@ -322,7 +322,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == u.add.channelId) assert(fwd.message.id == u.add.id) - assert(fwd.message.reason == Right(InvalidOnionBlinding(Sphinx.hash(u.add.onionRoutingPacket)))) + assert(fwd.message.reason == FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(u.add.onionRoutingPacket)))) assert(fwd.message.commit) }) peerConnection.expectMsgType[Warning] @@ -332,7 +332,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == u.add.channelId) assert(fwd.message.id == u.add.id) - assert(fwd.message.reason == Right(UnknownNextPeer())) + assert(fwd.message.reason == FailureReason.LocalFailure(UnknownNextPeer())) assert(fwd.message.commit) }) peerConnection.expectMsgType[Warning] @@ -402,7 +402,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwds = (0 until 5).map(_ => register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]) register.expectNoMessage(100 millis) fwds.foreach(fwd => { - assert(fwd.message.reason == Right(UnknownNextPeer())) + assert(fwd.message.reason == FailureReason.LocalFailure(UnknownNextPeer())) assert(fwd.message.commit) }) assert(fwds.map(_.channelId).toSet == (upstream1 ++ upstream2.slice(0, 1) ++ upstream3.received).map(_.add.channelId).toSet) @@ -1151,7 +1151,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == add.channelId) assert(fwd.message.id == add.id) - assert(fwd.message.reason == Right(UnknownNextPeer())) + assert(fwd.message.reason == FailureReason.LocalFailure(UnknownNextPeer())) assert(fwd.message.commit) }) register.expectNoMessage(100 millis) @@ -1176,7 +1176,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] assert(fwd.channelId == upstream1.add.channelId) assert(fwd.message.id == upstream1.add.id) - assert(fwd.message.reason == Right(UnknownNextPeer())) + assert(fwd.message.reason == FailureReason.LocalFailure(UnknownNextPeer())) assert(fwd.message.commit) register.expectNoMessage(100 millis) probe.expectNoMessage(100 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 16cb925a35..e2bab6caa8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -166,7 +166,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val fail = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]].message assert(fail.id == add_ab.id) - assert(fail.reason == Right(InvalidOnionBlinding(Sphinx.hash(add_ab.onionRoutingPacket)))) + assert(fail.reason == FailureReason.LocalFailure(InvalidOnionBlinding(Sphinx.hash(add_ab.onionRoutingPacket)))) assert(fail.delay_opt.nonEmpty) register.expectNoMessage(50 millis) @@ -202,7 +202,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val fail = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]].message assert(fail.id == add_ab.id) - assert(fail.reason == Right(RequiredNodeFeatureMissing())) + assert(fail.reason == FailureReason.LocalFailure(RequiredNodeFeatureMissing())) register.expectNoMessage(50 millis) } 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 7bcb786430..e6956f1d12 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 @@ -19,7 +19,7 @@ package fr.acinq.eclair.wire.internal import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.UInt64 import fr.acinq.eclair.channel._ -import fr.acinq.eclair.wire.protocol.{FailureMessageCodecs, FailureMessageTlv, GenericTlv, TemporaryNodeFailure, TlvStream} +import fr.acinq.eclair.wire.protocol._ import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -32,9 +32,9 @@ 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, Left(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44")) -> hex"0003 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", - CMD_FAIL_HTLC(253, Right(TemporaryNodeFailure())) -> hex"0003 00000000000000fd ff 0002 2002", - CMD_FAIL_HTLC(253, Right(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef")))))) -> hex"0003 00000000000000fd ff 0008 2002 1104deadbeef", + 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_MALFORMED_HTLC(7984, ByteVector32(hex"17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f3"), FailureMessageCodecs.BADONION) -> hex"0002 0000000000001f30 17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f38000", ) @@ -51,8 +51,11 @@ 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, Left(data123), None, commit = false, None), - hex"0001 000000000000002a900100" -> CMD_FAIL_HTLC(42, Right(TemporaryNodeFailure())), + 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"0002 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb01c8" -> CMD_FAIL_MALFORMED_HTLC(42, data32, 456, commit = false, None), )