Skip to content

Commit

Permalink
Refactor HTLC failure reason
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast committed Dec 3, 2024
1 parent 1870756 commit 3b7b421
Show file tree
Hide file tree
Showing 29 changed files with 236 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -127,8 +128,8 @@ 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,11 @@ 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 Left(forwarded) => Sphinx.FailurePacket.wrap(forwarded, sharedSecret)
case Right(failure) => Sphinx.FailurePacket.create(sharedSecret, failure)
case FailureReason.EncryptedDownstreamFailure(packet) => Sphinx.FailurePacket.wrap(packet, sharedSecret)
case FailureReason.LocalFailure(failure) => Sphinx.FailurePacket.create(sharedSecret, failure)
}
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand All @@ -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) =>
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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)
}
})
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -521,15 +521,15 @@ 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)
}

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)
}
Expand Down
Loading

0 comments on commit 3b7b421

Please sign in to comment.