Skip to content

Commit c9c5638

Browse files
authored
Add tlv stream to onion failures (#2455)
Extend every onion failure with an optional tlv stream. Added to the specification by: lightning/bolts#1021
1 parent 92c27fe commit c9c5638

32 files changed

+331
-291
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -874,11 +874,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
874874
case PostRevocationAction.RelayHtlc(add) =>
875875
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
876876
log.debug("closing in progress: failing {}", add)
877-
self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure), commit = true)
877+
self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), commit = true)
878878
case PostRevocationAction.RejectHtlc(add) =>
879879
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
880880
log.debug("closing in progress: rejecting {}", add)
881-
self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure), commit = true)
881+
self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), commit = true)
882882
case PostRevocationAction.RelayFailure(result) =>
883883
log.debug("forwarding {} to relayer", result)
884884
relayer ! result

eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class MultiPartPaymentFSM(nodeParams: NodeParams, paymentHash: ByteVector32, tot
5050
when(WAITING_FOR_HTLC) {
5151
case Event(PaymentTimeout, d: WaitingForHtlc) =>
5252
log.warning("multi-part payment timed out (received {} expected {})", d.paidAmount, totalAmount)
53-
goto(PAYMENT_FAILED) using PaymentFailed(protocol.PaymentTimeout, d.parts)
53+
goto(PAYMENT_FAILED) using PaymentFailed(protocol.PaymentTimeout(), d.parts)
5454

5555
case Event(part: PaymentPart, d: WaitingForHtlc) =>
5656
require(part.paymentHash == paymentHash, s"invalid payment hash (expected $paymentHash, received ${part.paymentHash}")

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,25 @@ object ChannelRelay {
7171
def translateLocalError(error: Throwable, channelUpdate_opt: Option[ChannelUpdate]): FailureMessage = {
7272
(error, channelUpdate_opt) match {
7373
case (_: ExpiryTooSmall, Some(channelUpdate)) => ExpiryTooSoon(channelUpdate)
74-
case (_: ExpiryTooBig, _) => ExpiryTooFar
74+
case (_: ExpiryTooBig, _) => ExpiryTooFar()
7575
case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
7676
case (_: TooManyAcceptedHtlcs, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
7777
case (_: HtlcValueTooHighInFlight, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
7878
case (_: LocalDustHtlcExposureTooHigh, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
7979
case (_: RemoteDustHtlcExposureTooHigh, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
8080
case (_: FeerateTooDifferent, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
8181
case (_: ChannelUnavailable, Some(channelUpdate)) if !channelUpdate.channelFlags.isEnabled => ChannelDisabled(channelUpdate.messageFlags, channelUpdate.channelFlags, channelUpdate)
82-
case (_: ChannelUnavailable, None) => PermanentChannelFailure
83-
case _ => TemporaryNodeFailure
82+
case (_: ChannelUnavailable, None) => PermanentChannelFailure()
83+
case _ => TemporaryNodeFailure()
8484
}
8585
}
8686

8787
def translateRelayFailure(originHtlcId: Long, fail: HtlcResult.Fail): CMD_FAIL_HTLC = {
8888
fail match {
8989
case f: HtlcResult.RemoteFail => CMD_FAIL_HTLC(originHtlcId, Left(f.fail.reason), commit = true)
9090
case f: HtlcResult.RemoteFailMalformed => CMD_FAIL_HTLC(originHtlcId, Right(createBadOnionFailure(f.fail.onionHash, f.fail.failureCode)), commit = true)
91-
case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure), commit = true)
92-
case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure), commit = true)
91+
case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure()), commit = true)
92+
case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure()), commit = true)
9393
case f: HtlcResult.DisconnectedBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(TemporaryChannelFailure(f.channelUpdate)), commit = true)
9494
}
9595
}
@@ -136,7 +136,7 @@ class ChannelRelay private(nodeParams: NodeParams,
136136
Behaviors.receiveMessagePartial {
137137
case WrappedForwardFailure(Register.ForwardFailure(Register.Forward(_, channelId, CMD_ADD_HTLC(_, _, _, _, _, _, o: Origin.ChannelRelayedHot, _)))) =>
138138
context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${o.add.id}")
139-
val cmdFail = CMD_FAIL_HTLC(o.add.id, Right(UnknownNextPeer), commit = true)
139+
val cmdFail = CMD_FAIL_HTLC(o.add.id, Right(UnknownNextPeer()), commit = true)
140140
Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel)
141141
safeSendAndStop(o.add.channelId, cmdFail)
142142

@@ -288,7 +288,7 @@ class ChannelRelay private(nodeParams: NodeParams,
288288
def relayOrFail(outgoingChannel_opt: Option[OutgoingChannelParams]): RelayResult = {
289289
outgoingChannel_opt match {
290290
case None =>
291-
RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true))
291+
RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), commit = true))
292292
case Some(c) if !c.channelUpdate.channelFlags.isEnabled =>
293293
RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(ChannelDisabled(c.channelUpdate.messageFlags, c.channelUpdate.channelFlags, c.channelUpdate)), commit = true))
294294
case Some(c) if r.amountToForward < c.channelUpdate.htlcMinimumMsat =>

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,11 @@ object NodeRelay {
108108
private def validateRelay(nodeParams: NodeParams, upstream: Upstream.Trampoline, payloadOut: IntermediatePayload.NodeRelay.Standard): Option[FailureMessage] = {
109109
val fee = nodeFee(nodeParams.relayParams.minTrampolineFees, payloadOut.amountToForward)
110110
if (upstream.amountIn - payloadOut.amountToForward < fee) {
111-
Some(TrampolineFeeInsufficient)
111+
Some(TrampolineFeeInsufficient())
112112
} else if (upstream.expiryIn - payloadOut.outgoingCltv < nodeParams.channelConf.expiryDelta) {
113-
Some(TrampolineExpiryTooSoon)
113+
Some(TrampolineExpiryTooSoon())
114114
} else if (payloadOut.outgoingCltv <= CltvExpiry(nodeParams.currentBlockHeight)) {
115-
Some(TrampolineExpiryTooSoon)
115+
Some(TrampolineExpiryTooSoon())
116116
} else if (payloadOut.invoiceFeatures.isDefined && payloadOut.paymentSecret.isEmpty) {
117117
Some(InvalidOnionPayload(UInt64(8), 0)) // payment secret field is missing
118118
} else if (payloadOut.amountToForward <= MilliSatoshi(0)) {
@@ -144,14 +144,14 @@ object NodeRelay {
144144
// We have direct channels to the target node, but not enough outgoing liquidity to use those channels.
145145
// The routing fee proposed by the sender was high enough to find alternative, indirect routes, but didn't yield
146146
// any result so we tell them that we don't have enough outgoing liquidity at the moment.
147-
Some(TemporaryNodeFailure)
148-
case LocalFailure(_, _, BalanceTooLow) :: Nil => Some(TrampolineFeeInsufficient) // a higher fee/cltv may find alternative, indirect routes
149-
case _ if routeNotFound => Some(TrampolineFeeInsufficient) // if we couldn't find routes, it's likely that the fee/cltv was insufficient
147+
Some(TemporaryNodeFailure())
148+
case LocalFailure(_, _, BalanceTooLow) :: Nil => Some(TrampolineFeeInsufficient()) // a higher fee/cltv may find alternative, indirect routes
149+
case _ if routeNotFound => Some(TrampolineFeeInsufficient()) // if we couldn't find routes, it's likely that the fee/cltv was insufficient
150150
case _ =>
151151
// Otherwise, we try to find a downstream error that we could decrypt.
152152
val outgoingNodeFailure = failures.collectFirst { case RemoteFailure(_, _, e) if e.originNode == nextPayload.outgoingNodeId => e.failureMessage }
153153
val otherNodeFailure = failures.collectFirst { case RemoteFailure(_, _, e) => e.failureMessage }
154-
val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure))
154+
val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure()))
155155
Some(failure)
156156
}
157157
}
@@ -224,11 +224,11 @@ class NodeRelay private(nodeParams: NodeParams,
224224
Behaviors.receiveMessagePartial {
225225
case WrappedPeerReadyResult(AsyncPaymentTriggerer.AsyncPaymentTimeout) =>
226226
context.log.warn("rejecting async payment; was not triggered before block {}", notifierTimeout)
227-
rejectPayment(upstream, Some(TemporaryNodeFailure)) // TODO: replace failure type when async payment spec is finalized
227+
rejectPayment(upstream, Some(TemporaryNodeFailure())) // TODO: replace failure type when async payment spec is finalized
228228
stopping()
229229
case WrappedPeerReadyResult(AsyncPaymentTriggerer.AsyncPaymentCanceled) =>
230230
context.log.warn(s"payment sender canceled a waiting async payment")
231-
rejectPayment(upstream, Some(TemporaryNodeFailure)) // TODO: replace failure type when async payment spec is finalized
231+
rejectPayment(upstream, Some(TemporaryNodeFailure())) // TODO: replace failure type when async payment spec is finalized
232232
stopping()
233233
case WrappedPeerReadyResult(AsyncPaymentTriggerer.AsyncPaymentTriggered) =>
234234
doSend(upstream, nextPayload, nextPacket)

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
116116
Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = false).increment()
117117
if (e.currentState != CLOSING && e.currentState != CLOSED) {
118118
log.info(s"failing not relayed htlc=$htlc")
119-
channel ! CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure), commit = true)
119+
channel ! CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure()), commit = true)
120120
} else {
121121
log.info(s"would fail but upstream channel is closed for htlc=$htlc")
122122
}
@@ -243,7 +243,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
243243
Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment()
244244
// We don't bother decrypting the downstream failure to forward a more meaningful error upstream, it's
245245
// very likely that it won't be actionable anyway because of our node restart.
246-
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true))
246+
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), commit = true))
247247
}
248248
}
249249
}

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym
7272
case Right(r: IncomingPaymentPacket.NodeRelayPacket) =>
7373
if (!nodeParams.enableTrampolinePayment) {
7474
log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} to nodeId=${r.innerPayload.outgoingNodeId} reason=trampoline disabled")
75-
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, Right(RequiredNodeFeatureMissing), commit = true))
75+
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, add.channelId, CMD_FAIL_HTLC(add.id, Right(RequiredNodeFeatureMissing()), commit = true))
7676
} else {
7777
nodeRelayer ! NodeRelayer.Relay(r)
7878
}
7979
case Left(badOnion: BadOnion) =>
8080
log.warning(s"couldn't parse onion: reason=${badOnion.message}")
8181
val cmdFail = badOnion match {
82-
case InvalidOnionBlinding(_) if add.blinding_opt.isEmpty =>
82+
case _: InvalidOnionBlinding if add.blinding_opt.isEmpty =>
8383
// We are the introduction point of a blinded path: we add a non-negligible delay to make it look like it
8484
// could come from a downstream node.
8585
val delay = Some(500.millis + Random.nextLong(1500).millis)

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package fr.acinq.eclair.payment.send
1919
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
2020
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2121
import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket
22-
import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, Bolt11Invoice, Invoice, RemoteFailure}
22+
import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentEvent, PaymentFailed, RemoteFailure}
2323
import fr.acinq.eclair.router.Router
2424
import fr.acinq.eclair.wire.protocol.IncorrectOrUnknownPaymentDetails
2525
import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TimestampSecond, randomBytes32, randomLong}
@@ -73,7 +73,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto
7373

7474
case paymentResult: PaymentEvent =>
7575
paymentResult match {
76-
case PaymentFailed(_, _, _ :+ RemoteFailure(_, _, DecryptedFailurePacket(targetNodeId, IncorrectOrUnknownPaymentDetails(_, _))), _) =>
76+
case PaymentFailed(_, _, _ :+ RemoteFailure(_, _, DecryptedFailurePacket(targetNodeId, _: IncorrectOrUnknownPaymentDetails)), _) =>
7777
log.info(s"payment probe successful to node=$targetNodeId")
7878
case _ =>
7979
log.info(s"payment probe failed with paymentResult=$paymentResult")

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,11 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
136136
case pp: PendingTrampolinePayment =>
137137
val trampolineHop = NodeHop(pp.r.trampolineNodeId, pp.r.recipientNodeId, pp.r.trampolineAttempts.last._2, pp.r.trampolineAttempts.last._1)
138138
val decryptedFailures = pf.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f)) => f }
139-
val shouldRetry = decryptedFailures.contains(TrampolineFeeInsufficient) || decryptedFailures.contains(TrampolineExpiryTooSoon)
139+
val shouldRetry = decryptedFailures.exists {
140+
case _: TrampolineFeeInsufficient => true
141+
case _: TrampolineExpiryTooSoon => true
142+
case _ => false
143+
}
140144
if (shouldRetry) {
141145
pp.remainingAttempts match {
142146
case (trampolineFees, trampolineExpiryDelta) :: remaining =>

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
181181
router ! Router.RouteCouldRelay(stoppedRoute)
182182
}
183183
failureMessage match {
184-
case TemporaryChannelFailure(update) =>
184+
case TemporaryChannelFailure(update, _) =>
185185
route.hops.find(_.nodeId == nodeId) match {
186186
case Some(failingHop) if HopRelayParams.areSame(failingHop.params, HopRelayParams.FromAnnouncement(update), ignoreHtlcSize = true) =>
187187
router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop)

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,50 @@ package fr.acinq.eclair.wire.internal
1919
import akka.actor.ActorRef
2020
import fr.acinq.eclair.channel._
2121
import fr.acinq.eclair.wire.protocol.CommonCodecs._
22-
import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.failureMessageCodec
22+
import fr.acinq.eclair.wire.protocol.FailureMessageCodecs._
23+
import fr.acinq.eclair.wire.protocol._
2324
import scodec.Codec
2425
import scodec.codecs._
2526

2627
import scala.concurrent.duration.FiniteDuration
2728

2829
object CommandCodecs {
2930

30-
val cmdFulfillCodec: Codec[CMD_FULFILL_HTLC] =
31+
// A trailing tlv stream was added in https://github.com/lightning/bolts/pull/1021 which wasn't handled properly by
32+
// our previous set of codecs because we didn't prefix failure messages with their length.
33+
private val legacyCmdFailCodec: Codec[CMD_FAIL_HTLC] =
34+
(("id" | int64) ::
35+
("reason" | either(bool, varsizebinarydata, provide(TemporaryNodeFailure()).upcast[FailureMessage])) ::
36+
("delay_opt" | provide(Option.empty[FiniteDuration])) ::
37+
("commit" | provide(false)) ::
38+
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC]
39+
40+
private val cmdFulfillCodec: Codec[CMD_FULFILL_HTLC] =
3141
(("id" | int64) ::
3242
("r" | bytes32) ::
3343
("commit" | provide(false)) ::
3444
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC]
3545

36-
val cmdFailCodec: Codec[CMD_FAIL_HTLC] =
46+
private val cmdFailCodec: Codec[CMD_FAIL_HTLC] =
3747
(("id" | int64) ::
38-
("reason" | either(bool, varsizebinarydata, failureMessageCodec)) ::
48+
("reason" | either(bool8, varsizebinarydata, variableSizeBytes(uint16, failureMessageCodec))) ::
3949
// No need to delay commands after a restart, we've been offline which already created a random delay.
4050
("delay_opt" | provide(Option.empty[FiniteDuration])) ::
4151
("commit" | provide(false)) ::
4252
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC]
4353

44-
val cmdFailMalformedCodec: Codec[CMD_FAIL_MALFORMED_HTLC] =
54+
private val cmdFailMalformedCodec: Codec[CMD_FAIL_MALFORMED_HTLC] =
4555
(("id" | int64) ::
4656
("onionHash" | bytes32) ::
4757
("failureCode" | uint16) ::
4858
("commit" | provide(false)) ::
4959
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_MALFORMED_HTLC]
5060

5161
val cmdCodec: Codec[HtlcSettlementCommand] = discriminated[HtlcSettlementCommand].by(uint16)
52-
.typecase(0, cmdFulfillCodec)
53-
.typecase(1, cmdFailCodec)
62+
// NB: order matters!
63+
.typecase(3, cmdFailCodec)
5464
.typecase(2, cmdFailMalformedCodec)
65+
.typecase(1, legacyCmdFailCodec)
66+
.typecase(0, cmdFulfillCodec)
5567

5668
}

0 commit comments

Comments
 (0)