Skip to content

Commit

Permalink
Add tlv stream to onion failures
Browse files Browse the repository at this point in the history
Extend every onion failure with an optional tlv stream.

See the specification here: lightning/bolts#1021
  • Loading branch information
t-bast committed Nov 22, 2022
1 parent 1e252e5 commit 755ad64
Show file tree
Hide file tree
Showing 31 changed files with 351 additions and 288 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -884,11 +884,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 @@ -70,13 +70,13 @@ 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 (_: 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()
}
}

Expand All @@ -95,8 +95,8 @@ object ChannelRelay {
case _ =>
CMD_FAIL_MALFORMED_HTLC(originHtlcId, 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 @@ -143,7 +143,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 @@ -277,7 +277,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 @@ -110,11 +110,11 @@ object NodeRelay {
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 @@ -146,14 +146,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 @@ -226,17 +226,17 @@ class NodeRelay private(nodeParams: NodeParams,
Behaviors.receiveMessagePartial {
case WrappedCurrentBlockHeight(blockHeight) if blockHeight >= safetyBlock =>
context.log.warn(s"rejecting async payment at block $blockHeight; was not triggered ${nodeParams.relayParams.asyncPaymentsParams.cancelSafetyBeforeTimeout} safety blocks before upstream cltv expiry at ${upstream.expiryIn}")
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 WrappedCurrentBlockHeight(blockHeight) if blockHeight >= timeoutBlock =>
context.log.warn(s"rejecting async payment at block $blockHeight; was not triggered after waiting ${nodeParams.relayParams.asyncPaymentsParams.holdTimeoutBlocks} blocks")
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 WrappedCurrentBlockHeight(blockHeight) =>
case _: WrappedCurrentBlockHeight =>
Behaviors.same
case CancelAsyncPayment =>
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 RelayAsyncPayment =>
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,7 +72,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} 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)
}
Expand All @@ -81,7 +81,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym
val delay_opt = badOnion match {
// 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.
case InvalidOnionBlinding(_) if add.blinding_opt.isEmpty => Some(500.millis + Random.nextLong(1500).millis)
case _: InvalidOnionBlinding if add.blinding_opt.isEmpty => Some(500.millis + Random.nextLong(1500).millis)
case _ => None
}
val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.code, delay_opt, commit = true)
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 @@ -130,7 +130,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
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())
if (shouldRetry) {
pp.remainingAttempts match {
case (trampolineFees, trampolineExpiryDelta) :: remaining =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
router ! Router.RouteCouldRelay(stoppedRoute)
}
failureMessage match {
case TemporaryChannelFailure(update) =>
case TemporaryChannelFailure(update, _) =>
d.route.hops.find(_.nodeId == nodeId) match {
case Some(failingHop) if ChannelRelayParams.areSame(failingHop.params, ChannelRelayParams.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,52 @@ 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
// 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)) ::
("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 +73,7 @@ object CommandCodecs {

val cmdFailCodec: Codec[CMD_FAIL_HTLC] =
(("id" | int64) ::
("reason" | either(bool, varsizebinarydata, failureMessageCodec)) ::
("reason" | either(bool8, varsizebinarydata, variableSizeBytes(uint16, failureMessageCodec))) ::
("commit" | provide(false)) ::
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC]

Expand All @@ -49,8 +87,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

0 comments on commit 755ad64

Please sign in to comment.