Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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,11 @@ 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.exists {
case _: TrampolineFeeInsufficient => true
case _: TrampolineExpiryTooSoon => true
case _ => false
}
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,38 +19,50 @@ 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._
import scodec.Codec
import scodec.codecs._

import scala.concurrent.duration.FiniteDuration

object CommandCodecs {

val cmdFulfillCodec: Codec[CMD_FULFILL_HTLC] =
// 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 legacyCmdFailCodec: Codec[CMD_FAIL_HTLC] =
(("id" | int64) ::
("reason" | either(bool, varsizebinarydata, provide(TemporaryNodeFailure()).upcast[FailureMessage])) ::
("delay_opt" | provide(Option.empty[FiniteDuration])) ::
("commit" | provide(false)) ::
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC]

private val cmdFulfillCodec: Codec[CMD_FULFILL_HTLC] =
(("id" | int64) ::
("r" | bytes32) ::
("commit" | provide(false)) ::
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC]

val cmdFailCodec: Codec[CMD_FAIL_HTLC] =
private 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)) ::
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FAIL_HTLC]

val cmdFailMalformedCodec: Codec[CMD_FAIL_MALFORMED_HTLC] =
private val cmdFailMalformedCodec: Codec[CMD_FAIL_MALFORMED_HTLC] =
(("id" | int64) ::
("onionHash" | bytes32) ::
("failureCode" | uint16) ::
("commit" | provide(false)) ::
("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