Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tlv stream to onion failures #2455

Merged
merged 4 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -874,11 +874,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
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, Right(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, Right(PermanentChannelFailure()), commit = true)
case PostRevocationAction.RelayFailure(result) =>
log.debug("forwarding {} to relayer", result)
relayer ! result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class MultiPartPaymentFSM(nodeParams: NodeParams, paymentHash: ByteVector32, tot
when(WAITING_FOR_HTLC) {
case Event(PaymentTimeout, d: WaitingForHtlc) =>
log.warning("multi-part payment timed out (received {} expected {})", d.paidAmount, totalAmount)
goto(PAYMENT_FAILED) using PaymentFailed(protocol.PaymentTimeout, d.parts)
goto(PAYMENT_FAILED) using PaymentFailed(protocol.PaymentTimeout(), d.parts)

case Event(part: PaymentPart, d: WaitingForHtlc) =>
require(part.paymentHash == paymentHash, s"invalid payment hash (expected $paymentHash, received ${part.paymentHash}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,25 @@ object ChannelRelay {
def translateLocalError(error: Throwable, channelUpdate_opt: Option[ChannelUpdate]): FailureMessage = {
(error, channelUpdate_opt) match {
case (_: ExpiryTooSmall, Some(channelUpdate)) => ExpiryTooSoon(channelUpdate)
case (_: ExpiryTooBig, _) => ExpiryTooFar
case (_: ExpiryTooBig, _) => ExpiryTooFar()
case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: TooManyAcceptedHtlcs, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: HtlcValueTooHighInFlight, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: LocalDustHtlcExposureTooHigh, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: RemoteDustHtlcExposureTooHigh, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: FeerateTooDifferent, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: ChannelUnavailable, Some(channelUpdate)) if !channelUpdate.channelFlags.isEnabled => ChannelDisabled(channelUpdate.messageFlags, channelUpdate.channelFlags, channelUpdate)
case (_: ChannelUnavailable, None) => PermanentChannelFailure
case _ => TemporaryNodeFailure
case (_: ChannelUnavailable, None) => PermanentChannelFailure()
case _ => TemporaryNodeFailure()
}
}

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 _: 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(f.channelUpdate)), commit = true)
}
}
Expand Down Expand Up @@ -136,7 +136,7 @@ class ChannelRelay private(nodeParams: NodeParams,
Behaviors.receiveMessagePartial {
case WrappedForwardFailure(Register.ForwardFailure(Register.Forward(_, channelId, CMD_ADD_HTLC(_, _, _, _, _, _, o: Origin.ChannelRelayedHot, _)))) =>
context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${o.add.id}")
val cmdFail = CMD_FAIL_HTLC(o.add.id, Right(UnknownNextPeer), commit = true)
val cmdFail = CMD_FAIL_HTLC(o.add.id, Right(UnknownNextPeer()), commit = true)
Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel)
safeSendAndStop(o.add.channelId, cmdFail)

Expand Down Expand Up @@ -288,7 +288,7 @@ class ChannelRelay private(nodeParams: NodeParams,
def relayOrFail(outgoingChannel_opt: Option[OutgoingChannelParams]): RelayResult = {
outgoingChannel_opt match {
case None =>
RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true))
RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), commit = true))
case Some(c) if !c.channelUpdate.channelFlags.isEnabled =>
RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(ChannelDisabled(c.channelUpdate.messageFlags, c.channelUpdate.channelFlags, c.channelUpdate)), commit = true))
case Some(c) if r.amountToForward < c.channelUpdate.htlcMinimumMsat =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ object NodeRelay {
private def validateRelay(nodeParams: NodeParams, upstream: Upstream.Trampoline, payloadOut: IntermediatePayload.NodeRelay.Standard): Option[FailureMessage] = {
val fee = nodeFee(nodeParams.relayParams.minTrampolineFees, payloadOut.amountToForward)
if (upstream.amountIn - payloadOut.amountToForward < fee) {
Some(TrampolineFeeInsufficient)
Some(TrampolineFeeInsufficient())
} else if (upstream.expiryIn - payloadOut.outgoingCltv < nodeParams.channelConf.expiryDelta) {
Some(TrampolineExpiryTooSoon)
Some(TrampolineExpiryTooSoon())
} else if (payloadOut.outgoingCltv <= CltvExpiry(nodeParams.currentBlockHeight)) {
Some(TrampolineExpiryTooSoon)
Some(TrampolineExpiryTooSoon())
} else if (payloadOut.invoiceFeatures.isDefined && payloadOut.paymentSecret.isEmpty) {
Some(InvalidOnionPayload(UInt64(8), 0)) // payment secret field is missing
} else if (payloadOut.amountToForward <= MilliSatoshi(0)) {
Expand Down Expand Up @@ -144,14 +144,14 @@ object NodeRelay {
// We have direct channels to the target node, but not enough outgoing liquidity to use those channels.
// The routing fee proposed by the sender was high enough to find alternative, indirect routes, but didn't yield
// any result so we tell them that we don't have enough outgoing liquidity at the moment.
Some(TemporaryNodeFailure)
case LocalFailure(_, _, BalanceTooLow) :: Nil => Some(TrampolineFeeInsufficient) // a higher fee/cltv may find alternative, indirect routes
case _ if routeNotFound => Some(TrampolineFeeInsufficient) // if we couldn't find routes, it's likely that the fee/cltv was insufficient
Some(TemporaryNodeFailure())
case LocalFailure(_, _, BalanceTooLow) :: Nil => Some(TrampolineFeeInsufficient()) // a higher fee/cltv may find alternative, indirect routes
case _ if routeNotFound => Some(TrampolineFeeInsufficient()) // if we couldn't find routes, it's likely that the fee/cltv was insufficient
case _ =>
// Otherwise, we try to find a downstream error that we could decrypt.
val outgoingNodeFailure = failures.collectFirst { case RemoteFailure(_, _, e) if e.originNode == nextPayload.outgoingNodeId => e.failureMessage }
val otherNodeFailure = failures.collectFirst { case RemoteFailure(_, _, e) => e.failureMessage }
val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure))
val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure()))
Some(failure)
}
}
Expand Down Expand Up @@ -224,11 +224,11 @@ class NodeRelay private(nodeParams: NodeParams,
Behaviors.receiveMessagePartial {
case WrappedPeerReadyResult(AsyncPaymentTriggerer.AsyncPaymentTimeout) =>
context.log.warn("rejecting async payment; was not triggered before block {}", notifierTimeout)
rejectPayment(upstream, Some(TemporaryNodeFailure)) // TODO: replace failure type when async payment spec is finalized
rejectPayment(upstream, Some(TemporaryNodeFailure())) // TODO: replace failure type when async payment spec is finalized
stopping()
case WrappedPeerReadyResult(AsyncPaymentTriggerer.AsyncPaymentCanceled) =>
context.log.warn(s"payment sender canceled a waiting async payment")
rejectPayment(upstream, Some(TemporaryNodeFailure)) // TODO: replace failure type when async payment spec is finalized
rejectPayment(upstream, Some(TemporaryNodeFailure())) // TODO: replace failure type when async payment spec is finalized
stopping()
case WrappedPeerReadyResult(AsyncPaymentTriggerer.AsyncPaymentTriggered) =>
doSend(upstream, nextPayload, nextPacket)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = false).increment()
if (e.currentState != CLOSING && e.currentState != CLOSED) {
log.info(s"failing not relayed htlc=$htlc")
channel ! CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure), commit = true)
channel ! CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), commit = true)
} else {
log.info(s"would fail but upstream channel is closed for htlc=$htlc")
}
Expand Down Expand Up @@ -243,7 +243,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, Right(TemporaryNodeFailure()), commit = true))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ 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} to nodeId=${r.innerPayload.outgoingNodeId} 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, Right(RequiredNodeFeatureMissing()), commit = true))
} else {
nodeRelayer ! NodeRelayer.Relay(r)
}
case Left(badOnion: BadOnion) =>
log.warning(s"couldn't parse onion: reason=${badOnion.message}")
val cmdFail = badOnion match {
case InvalidOnionBlinding(_) if add.blinding_opt.isEmpty =>
case _: InvalidOnionBlinding if add.blinding_opt.isEmpty =>
// 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package fr.acinq.eclair.payment.send
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket
import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, Bolt11Invoice, Invoice, RemoteFailure}
import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentEvent, PaymentFailed, RemoteFailure}
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.wire.protocol.IncorrectOrUnknownPaymentDetails
import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TimestampSecond, randomBytes32, randomLong}
Expand Down Expand Up @@ -73,7 +73,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto

case paymentResult: PaymentEvent =>
paymentResult match {
case PaymentFailed(_, _, _ :+ RemoteFailure(_, _, DecryptedFailurePacket(targetNodeId, IncorrectOrUnknownPaymentDetails(_, _))), _) =>
case PaymentFailed(_, _, _ :+ RemoteFailure(_, _, DecryptedFailurePacket(targetNodeId, _: IncorrectOrUnknownPaymentDetails)), _) =>
log.info(s"payment probe successful to node=$targetNodeId")
case _ =>
log.info(s"payment probe failed with paymentResult=$paymentResult")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
case pp: PendingTrampolinePayment =>
val trampolineHop = NodeHop(pp.r.trampolineNodeId, pp.r.recipientNodeId, pp.r.trampolineAttempts.last._2, pp.r.trampolineAttempts.last._1)
val decryptedFailures = pf.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f)) => f }
val shouldRetry = decryptedFailures.contains(TrampolineFeeInsufficient) || decryptedFailures.contains(TrampolineExpiryTooSoon)
val shouldRetry = decryptedFailures.contains(TrampolineFeeInsufficient()) || decryptedFailures.contains(TrampolineExpiryTooSoon())
pm47 marked this conversation as resolved.
Show resolved Hide resolved
if (shouldRetry) {
pp.remainingAttempts match {
case (trampolineFees, trampolineExpiryDelta) :: remaining =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
router ! Router.RouteCouldRelay(stoppedRoute)
}
failureMessage match {
case TemporaryChannelFailure(update) =>
case TemporaryChannelFailure(update, _) =>
route.hops.find(_.nodeId == nodeId) match {
case Some(failingHop) if HopRelayParams.areSame(failingHop.params, HopRelayParams.FromAnnouncement(update), ignoreHtlcSize = true) =>
router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,53 @@ package fr.acinq.eclair.wire.internal
import akka.actor.ActorRef
import fr.acinq.eclair.channel._
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.failureMessageCodec
import fr.acinq.eclair.wire.protocol.FailureMessageCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelFlagsCodec, messageFlagsCodec}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong}
import scodec.Codec
import scodec.codecs._

import scala.concurrent.duration.FiniteDuration

object CommandCodecs {

// A trailing tlv stream was added in https://github.com/lightning/bolts/pull/1021 which wasn't handled properly by
pm47 marked this conversation as resolved.
Show resolved Hide resolved
// our previous set of codecs because we didn't prefix failure messages with their length.
private val legacyFailureMessageCodec = discriminated[FailureMessage].by(uint16)
.typecase(PERM | 1, provide(InvalidRealm()))
.typecase(NODE | 2, provide(TemporaryNodeFailure()))
.typecase(PERM | NODE | 2, provide(PermanentNodeFailure()))
.typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing()))
.typecase(BADONION | PERM | 4, (sha256 :: provide(TlvStream.empty[FailureMessageTlv])).as[InvalidOnionVersion])
.typecase(BADONION | PERM | 5, (sha256 :: provide(TlvStream.empty[FailureMessageTlv])).as[InvalidOnionHmac])
.typecase(BADONION | PERM | 6, (sha256 :: provide(TlvStream.empty[FailureMessageTlv])).as[InvalidOnionKey])
.typecase(UPDATE | 7, (channelUpdateWithLengthCodec :: provide(TlvStream.empty[FailureMessageTlv])).as[TemporaryChannelFailure])
.typecase(PERM | 8, provide(PermanentChannelFailure()))
.typecase(PERM | 9, provide(RequiredChannelFeatureMissing()))
.typecase(PERM | 10, provide(UnknownNextPeer()))
.typecase(UPDATE | 11, (("amountMsat" | millisatoshi) :: ("channelUpdate" | channelUpdateWithLengthCodec) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[AmountBelowMinimum])
.typecase(UPDATE | 12, (("amountMsat" | millisatoshi) :: ("channelUpdate" | channelUpdateWithLengthCodec) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[FeeInsufficient])
.typecase(UPDATE | 13, (("expiry" | cltvExpiry) :: ("channelUpdate" | channelUpdateWithLengthCodec) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[IncorrectCltvExpiry])
.typecase(UPDATE | 14, (("channelUpdate" | channelUpdateWithLengthCodec) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[ExpiryTooSoon])
.typecase(UPDATE | 20, (messageFlagsCodec :: channelFlagsCodec :: ("channelUpdate" | channelUpdateWithLengthCodec) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[ChannelDisabled])
.typecase(PERM | 15, (("amountMsat" | withDefaultValue(optional(bitsRemaining, millisatoshi), 0 msat)) :: ("height" | withDefaultValue(optional(bitsRemaining, blockHeight), BlockHeight(0))) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[IncorrectOrUnknownPaymentDetails])
.typecase(18, (("expiry" | cltvExpiry) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[FinalIncorrectCltvExpiry])
.typecase(19, (("amountMsat" | millisatoshi) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[FinalIncorrectHtlcAmount])
.typecase(21, provide(ExpiryTooFar()))
.typecase(PERM | 22, (("tag" | varint) :: ("offset" | uint16) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[InvalidOnionPayload])
.typecase(23, provide(PaymentTimeout()))
.typecase(BADONION | PERM | 24, (sha256 :: provide(TlvStream.empty[FailureMessageTlv])).as[InvalidOnionBlinding])
.typecase(NODE | 51, provide(TrampolineFeeInsufficient()))
.typecase(NODE | 52, provide(TrampolineExpiryTooSoon()))

private val legacyCmdFailCodec: Codec[CMD_FAIL_HTLC] =
(("id" | int64) ::
("reason" | either(bool, varsizebinarydata, legacyFailureMessageCodec)) ::
("delay_opt" | provide(Option.empty[FiniteDuration])) ::
("commit" | provide(false)) ::
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC]

val cmdFulfillCodec: Codec[CMD_FULFILL_HTLC] =
(("id" | int64) ::
("r" | bytes32) ::
Expand All @@ -35,7 +74,7 @@ object CommandCodecs {

val cmdFailCodec: Codec[CMD_FAIL_HTLC] =
(("id" | int64) ::
("reason" | either(bool, varsizebinarydata, failureMessageCodec)) ::
("reason" | either(bool8, varsizebinarydata, variableSizeBytes(uint16, failureMessageCodec))) ::
// 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)) ::
Expand All @@ -49,8 +88,10 @@ object CommandCodecs {
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_MALFORMED_HTLC]

val cmdCodec: Codec[HtlcSettlementCommand] = discriminated[HtlcSettlementCommand].by(uint16)
.typecase(0, cmdFulfillCodec)
.typecase(1, cmdFailCodec)
// NB: order matters!
.typecase(3, cmdFailCodec)
.typecase(2, cmdFailMalformedCodec)
.typecase(1, legacyCmdFailCodec)
.typecase(0, cmdFulfillCodec)

}
Loading