Skip to content

Commit 755ad64

Browse files
committed
Add tlv stream to onion failures
Extend every onion failure with an optional tlv stream. See the specification here: lightning/bolts#1021
1 parent 1e252e5 commit 755ad64

31 files changed

+351
-288
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
@@ -884,11 +884,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
884884
case PostRevocationAction.RelayHtlc(add) =>
885885
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
886886
log.debug("closing in progress: failing {}", add)
887-
self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure), commit = true)
887+
self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), commit = true)
888888
case PostRevocationAction.RejectHtlc(add) =>
889889
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
890890
log.debug("closing in progress: rejecting {}", add)
891-
self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure), commit = true)
891+
self ! CMD_FAIL_HTLC(add.id, Right(PermanentChannelFailure()), commit = true)
892892
case PostRevocationAction.RelayFailure(result) =>
893893
log.debug("forwarding {} to relayer", result)
894894
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
@@ -70,13 +70,13 @@ object ChannelRelay {
7070
def translateLocalError(error: Throwable, channelUpdate_opt: Option[ChannelUpdate]): FailureMessage = {
7171
(error, channelUpdate_opt) match {
7272
case (_: ExpiryTooSmall, Some(channelUpdate)) => ExpiryTooSoon(channelUpdate)
73-
case (_: ExpiryTooBig, _) => ExpiryTooFar
73+
case (_: ExpiryTooBig, _) => ExpiryTooFar()
7474
case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
7575
case (_: TooManyAcceptedHtlcs, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
7676
case (_: FeerateTooDifferent, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
7777
case (_: ChannelUnavailable, Some(channelUpdate)) if !channelUpdate.channelFlags.isEnabled => ChannelDisabled(channelUpdate.messageFlags, channelUpdate.channelFlags, channelUpdate)
78-
case (_: ChannelUnavailable, None) => PermanentChannelFailure
79-
case _ => TemporaryNodeFailure
78+
case (_: ChannelUnavailable, None) => PermanentChannelFailure()
79+
case _ => TemporaryNodeFailure()
8080
}
8181
}
8282

@@ -95,8 +95,8 @@ object ChannelRelay {
9595
case _ =>
9696
CMD_FAIL_MALFORMED_HTLC(originHtlcId, f.fail.onionHash, f.fail.failureCode, commit = true)
9797
}
98-
case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure), commit = true)
99-
case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure), commit = true)
98+
case _: HtlcResult.OnChainFail => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure()), commit = true)
99+
case HtlcResult.ChannelFailureBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure()), commit = true)
100100
case f: HtlcResult.DisconnectedBeforeSigned => CMD_FAIL_HTLC(originHtlcId, Right(TemporaryChannelFailure(f.channelUpdate)), commit = true)
101101
}
102102
}
@@ -143,7 +143,7 @@ class ChannelRelay private(nodeParams: NodeParams,
143143
Behaviors.receiveMessagePartial {
144144
case WrappedForwardFailure(Register.ForwardFailure(Register.Forward(_, channelId, CMD_ADD_HTLC(_, _, _, _, _, _, o: Origin.ChannelRelayedHot, _)))) =>
145145
context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${o.add.id}")
146-
val cmdFail = CMD_FAIL_HTLC(o.add.id, Right(UnknownNextPeer), commit = true)
146+
val cmdFail = CMD_FAIL_HTLC(o.add.id, Right(UnknownNextPeer()), commit = true)
147147
Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel)
148148
safeSendAndStop(o.add.channelId, cmdFail)
149149

@@ -277,7 +277,7 @@ class ChannelRelay private(nodeParams: NodeParams,
277277
def relayOrFail(outgoingChannel_opt: Option[OutgoingChannelParams]): RelayResult = {
278278
outgoingChannel_opt match {
279279
case None =>
280-
RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true))
280+
RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer()), commit = true))
281281
case Some(c) if !c.channelUpdate.channelFlags.isEnabled =>
282282
RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(ChannelDisabled(c.channelUpdate.messageFlags, c.channelUpdate.channelFlags, c.channelUpdate)), commit = true))
283283
case Some(c) if r.amountToForward < c.channelUpdate.htlcMinimumMsat =>

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,11 @@ object NodeRelay {
110110
def validateRelay(nodeParams: NodeParams, upstream: Upstream.Trampoline, payloadOut: IntermediatePayload.NodeRelay.Standard): Option[FailureMessage] = {
111111
val fee = nodeFee(nodeParams.relayParams.minTrampolineFees, payloadOut.amountToForward)
112112
if (upstream.amountIn - payloadOut.amountToForward < fee) {
113-
Some(TrampolineFeeInsufficient)
113+
Some(TrampolineFeeInsufficient())
114114
} else if (upstream.expiryIn - payloadOut.outgoingCltv < nodeParams.channelConf.expiryDelta) {
115-
Some(TrampolineExpiryTooSoon)
115+
Some(TrampolineExpiryTooSoon())
116116
} else if (payloadOut.outgoingCltv <= CltvExpiry(nodeParams.currentBlockHeight)) {
117-
Some(TrampolineExpiryTooSoon)
117+
Some(TrampolineExpiryTooSoon())
118118
} else if (payloadOut.invoiceFeatures.isDefined && payloadOut.paymentSecret.isEmpty) {
119119
Some(InvalidOnionPayload(UInt64(8), 0)) // payment secret field is missing
120120
} else if (payloadOut.amountToForward <= MilliSatoshi(0)) {
@@ -146,14 +146,14 @@ object NodeRelay {
146146
// We have direct channels to the target node, but not enough outgoing liquidity to use those channels.
147147
// The routing fee proposed by the sender was high enough to find alternative, indirect routes, but didn't yield
148148
// any result so we tell them that we don't have enough outgoing liquidity at the moment.
149-
Some(TemporaryNodeFailure)
150-
case LocalFailure(_, _, BalanceTooLow) :: Nil => Some(TrampolineFeeInsufficient) // a higher fee/cltv may find alternative, indirect routes
151-
case _ if routeNotFound => Some(TrampolineFeeInsufficient) // if we couldn't find routes, it's likely that the fee/cltv was insufficient
149+
Some(TemporaryNodeFailure())
150+
case LocalFailure(_, _, BalanceTooLow) :: Nil => Some(TrampolineFeeInsufficient()) // a higher fee/cltv may find alternative, indirect routes
151+
case _ if routeNotFound => Some(TrampolineFeeInsufficient()) // if we couldn't find routes, it's likely that the fee/cltv was insufficient
152152
case _ =>
153153
// Otherwise, we try to find a downstream error that we could decrypt.
154154
val outgoingNodeFailure = failures.collectFirst { case RemoteFailure(_, _, e) if e.originNode == nextPayload.outgoingNodeId => e.failureMessage }
155155
val otherNodeFailure = failures.collectFirst { case RemoteFailure(_, _, e) => e.failureMessage }
156-
val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure))
156+
val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure()))
157157
Some(failure)
158158
}
159159
}
@@ -226,17 +226,17 @@ class NodeRelay private(nodeParams: NodeParams,
226226
Behaviors.receiveMessagePartial {
227227
case WrappedCurrentBlockHeight(blockHeight) if blockHeight >= safetyBlock =>
228228
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}")
229-
rejectPayment(upstream, Some(TemporaryNodeFailure)) // TODO: replace failure type when async payment spec is finalized
229+
rejectPayment(upstream, Some(TemporaryNodeFailure())) // TODO: replace failure type when async payment spec is finalized
230230
stopping()
231231
case WrappedCurrentBlockHeight(blockHeight) if blockHeight >= timeoutBlock =>
232232
context.log.warn(s"rejecting async payment at block $blockHeight; was not triggered after waiting ${nodeParams.relayParams.asyncPaymentsParams.holdTimeoutBlocks} blocks")
233-
rejectPayment(upstream, Some(TemporaryNodeFailure)) // TODO: replace failure type when async payment spec is finalized
233+
rejectPayment(upstream, Some(TemporaryNodeFailure())) // TODO: replace failure type when async payment spec is finalized
234234
stopping()
235-
case WrappedCurrentBlockHeight(blockHeight) =>
235+
case _: WrappedCurrentBlockHeight =>
236236
Behaviors.same
237237
case CancelAsyncPayment =>
238238
context.log.warn(s"payment sender canceled a waiting async payment")
239-
rejectPayment(upstream, Some(TemporaryNodeFailure)) // TODO: replace failure type when async payment spec is finalized
239+
rejectPayment(upstream, Some(TemporaryNodeFailure())) // TODO: replace failure type when async payment spec is finalized
240240
stopping()
241241
case RelayAsyncPayment =>
242242
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,7 +72,7 @@ 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
}
@@ -81,7 +81,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym
8181
val delay_opt = badOnion match {
8282
// We are the introduction point of a blinded path: we add a non-negligible delay to make it look like it
8383
// could come from a downstream node.
84-
case InvalidOnionBlinding(_) if add.blinding_opt.isEmpty => Some(500.millis + Random.nextLong(1500).millis)
84+
case _: InvalidOnionBlinding if add.blinding_opt.isEmpty => Some(500.millis + Random.nextLong(1500).millis)
8585
case _ => None
8686
}
8787
val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.code, delay_opt, commit = true)

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
130130
NodeHop(pp.r.trampolineNodeId, pp.r.recipientNodeId, pp.r.trampolineAttempts.last._2, pp.r.trampolineAttempts.last._1)
131131
)
132132
val decryptedFailures = pf.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f)) => f }
133-
val shouldRetry = decryptedFailures.contains(TrampolineFeeInsufficient) || decryptedFailures.contains(TrampolineExpiryTooSoon)
133+
val shouldRetry = decryptedFailures.contains(TrampolineFeeInsufficient()) || decryptedFailures.contains(TrampolineExpiryTooSoon())
134134
if (shouldRetry) {
135135
pp.remainingAttempts match {
136136
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
@@ -183,7 +183,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
183183
router ! Router.RouteCouldRelay(stoppedRoute)
184184
}
185185
failureMessage match {
186-
case TemporaryChannelFailure(update) =>
186+
case TemporaryChannelFailure(update, _) =>
187187
d.route.hops.find(_.nodeId == nodeId) match {
188188
case Some(failingHop) if ChannelRelayParams.areSame(failingHop.params, ChannelRelayParams.FromAnnouncement(update), ignoreHtlcSize = true) =>
189189
router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop)

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

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,52 @@ 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.LightningMessageCodecs.{channelFlagsCodec, messageFlagsCodec}
24+
import fr.acinq.eclair.wire.protocol._
25+
import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong}
2326
import scodec.Codec
2427
import scodec.codecs._
2528

2629
import scala.concurrent.duration.FiniteDuration
2730

2831
object CommandCodecs {
2932

33+
// A trailing tlv stream was added in https://github.com/lightning/bolts/pull/1021 which wasn't handled properly by
34+
// our previous set of codecs because we didn't prefix failure messages with their length.
35+
private val legacyFailureMessageCodec = discriminated[FailureMessage].by(uint16)
36+
.typecase(PERM | 1, provide(InvalidRealm()))
37+
.typecase(NODE | 2, provide(TemporaryNodeFailure()))
38+
.typecase(PERM | NODE | 2, provide(PermanentNodeFailure()))
39+
.typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing()))
40+
.typecase(BADONION | PERM | 4, (sha256 :: provide(TlvStream.empty[FailureMessageTlv])).as[InvalidOnionVersion])
41+
.typecase(BADONION | PERM | 5, (sha256 :: provide(TlvStream.empty[FailureMessageTlv])).as[InvalidOnionHmac])
42+
.typecase(BADONION | PERM | 6, (sha256 :: provide(TlvStream.empty[FailureMessageTlv])).as[InvalidOnionKey])
43+
.typecase(UPDATE | 7, (channelUpdateWithLengthCodec :: provide(TlvStream.empty[FailureMessageTlv])).as[TemporaryChannelFailure])
44+
.typecase(PERM | 8, provide(PermanentChannelFailure()))
45+
.typecase(PERM | 9, provide(RequiredChannelFeatureMissing()))
46+
.typecase(PERM | 10, provide(UnknownNextPeer()))
47+
.typecase(UPDATE | 11, (("amountMsat" | millisatoshi) :: ("channelUpdate" | channelUpdateWithLengthCodec) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[AmountBelowMinimum])
48+
.typecase(UPDATE | 12, (("amountMsat" | millisatoshi) :: ("channelUpdate" | channelUpdateWithLengthCodec) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[FeeInsufficient])
49+
.typecase(UPDATE | 13, (("expiry" | cltvExpiry) :: ("channelUpdate" | channelUpdateWithLengthCodec) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[IncorrectCltvExpiry])
50+
.typecase(UPDATE | 14, (("channelUpdate" | channelUpdateWithLengthCodec) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[ExpiryTooSoon])
51+
.typecase(UPDATE | 20, (messageFlagsCodec :: channelFlagsCodec :: ("channelUpdate" | channelUpdateWithLengthCodec) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[ChannelDisabled])
52+
.typecase(PERM | 15, (("amountMsat" | withDefaultValue(optional(bitsRemaining, millisatoshi), 0 msat)) :: ("height" | withDefaultValue(optional(bitsRemaining, blockHeight), BlockHeight(0))) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[IncorrectOrUnknownPaymentDetails])
53+
.typecase(18, (("expiry" | cltvExpiry) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[FinalIncorrectCltvExpiry])
54+
.typecase(19, (("amountMsat" | millisatoshi) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[FinalIncorrectHtlcAmount])
55+
.typecase(21, provide(ExpiryTooFar()))
56+
.typecase(PERM | 22, (("tag" | varint) :: ("offset" | uint16) :: ("tlvs" | provide(TlvStream.empty[FailureMessageTlv]))).as[InvalidOnionPayload])
57+
.typecase(23, provide(PaymentTimeout()))
58+
.typecase(BADONION | PERM | 24, (sha256 :: provide(TlvStream.empty[FailureMessageTlv])).as[InvalidOnionBlinding])
59+
.typecase(NODE | 51, provide(TrampolineFeeInsufficient()))
60+
.typecase(NODE | 52, provide(TrampolineExpiryTooSoon()))
61+
62+
private val legacyCmdFailCodec: Codec[CMD_FAIL_HTLC] =
63+
(("id" | int64) ::
64+
("reason" | either(bool, varsizebinarydata, legacyFailureMessageCodec)) ::
65+
("commit" | provide(false)) ::
66+
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC]
67+
3068
val cmdFulfillCodec: Codec[CMD_FULFILL_HTLC] =
3169
(("id" | int64) ::
3270
("r" | bytes32) ::
@@ -35,7 +73,7 @@ object CommandCodecs {
3573

3674
val cmdFailCodec: Codec[CMD_FAIL_HTLC] =
3775
(("id" | int64) ::
38-
("reason" | either(bool, varsizebinarydata, failureMessageCodec)) ::
76+
("reason" | either(bool8, varsizebinarydata, variableSizeBytes(uint16, failureMessageCodec))) ::
3977
("commit" | provide(false)) ::
4078
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC]
4179

@@ -49,8 +87,10 @@ object CommandCodecs {
4987
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_MALFORMED_HTLC]
5088

5189
val cmdCodec: Codec[HtlcSettlementCommand] = discriminated[HtlcSettlementCommand].by(uint16)
52-
.typecase(0, cmdFulfillCodec)
53-
.typecase(1, cmdFailCodec)
90+
// NB: order matters!
91+
.typecase(3, cmdFailCodec)
5492
.typecase(2, cmdFailMalformedCodec)
93+
.typecase(1, legacyCmdFailCodec)
94+
.typecase(0, cmdFulfillCodec)
5595

5696
}

0 commit comments

Comments
 (0)