@@ -126,12 +126,15 @@ object NodeRelay {
126126 val amountOut = outgoingAmount(upstream, payloadOut)
127127 val expiryOut = outgoingExpiry(upstream, payloadOut)
128128 val fee = nodeFee(nodeParams.relayParams.minTrampolineFees, amountOut)
129+ // We don't know yet how costly it is to reach the next node: we use a rough first estimate of twice our trampoline
130+ // fees. If we fail to find routes, we will return a different error with higher fees and expiry delta.
131+ val failure = TrampolineFeeOrExpiryInsufficient (nodeParams.relayParams.minTrampolineFees.feeBase * 2 , nodeParams.relayParams.minTrampolineFees.feeProportionalMillionths * 2 , nodeParams.channelConf.expiryDelta * 2 )
129132 if (upstream.amountIn - amountOut < fee) {
130- Some (TrampolineFeeInsufficient () )
133+ Some (failure )
131134 } else if (upstream.expiryIn - expiryOut < nodeParams.channelConf.expiryDelta) {
132- Some (TrampolineExpiryTooSoon () )
135+ Some (failure )
133136 } else if (expiryOut <= CltvExpiry (nodeParams.currentBlockHeight)) {
134- Some (TrampolineExpiryTooSoon () )
137+ Some (failure )
135138 } else if (amountOut <= MilliSatoshi (0 )) {
136139 Some (InvalidOnionPayload (UInt64 (2 ), 0 ))
137140 } else {
@@ -181,31 +184,40 @@ object NodeRelay {
181184 * This helper method translates relaying errors (returned by the downstream nodes) to a BOLT 4 standard error that we
182185 * should return upstream.
183186 */
184- private def translateError (nodeParams : NodeParams , failures : Seq [PaymentFailure ], upstream : Upstream .Hot .Trampoline , nextPayload : IntermediatePayload .NodeRelay ): Option [ FailureMessage ] = {
187+ private def translateError (nodeParams : NodeParams , failures : Seq [PaymentFailure ], upstream : Upstream .Hot .Trampoline , nextPayload : IntermediatePayload .NodeRelay ): FailureReason = {
185188 val amountOut = outgoingAmount(upstream, nextPayload)
186189 val routeNotFound = failures.collectFirst { case f@ LocalFailure (_, _, RouteNotFound ) => f }.nonEmpty
187190 val routingFeeHigh = upstream.amountIn - amountOut >= nodeFee(nodeParams.relayParams.minTrampolineFees, amountOut) * 5
191+ val trampolineFeesFailure = TrampolineFeeOrExpiryInsufficient (nodeParams.relayParams.minTrampolineFees.feeBase * 5 , nodeParams.relayParams.minTrampolineFees.feeProportionalMillionths * 5 , nodeParams.channelConf.expiryDelta * 5 )
192+ // We select the best error we can from our downstream attempts.
188193 failures match {
189- case Nil => None
194+ case Nil => FailureReason . LocalTrampolineFailure ( TemporaryTrampolineFailure ())
190195 case LocalFailure (_, _, BalanceTooLow ) :: Nil if routingFeeHigh =>
191196 // We have direct channels to the target node, but not enough outgoing liquidity to use those channels.
192- // The routing fee proposed by the sender was high enough to find alternative, indirect routes, but didn't yield
193- // any result so we tell them that we don't have enough outgoing liquidity at the moment.
194- Some (TemporaryNodeFailure ())
195- case LocalFailure (_, _, BalanceTooLow ) :: Nil => Some (TrampolineFeeInsufficient ()) // a higher fee/cltv may find alternative, indirect routes
196- case _ if routeNotFound => Some (TrampolineFeeInsufficient ()) // if we couldn't find routes, it's likely that the fee/cltv was insufficient
197+ // The routing fee proposed by the sender was high enough to find alternative, indirect routes, but didn't
198+ // yield any result so we tell them that we don't have enough outgoing liquidity at the moment.
199+ FailureReason .LocalTrampolineFailure (TemporaryTrampolineFailure ())
200+ case LocalFailure (_, _, BalanceTooLow ) :: Nil =>
201+ // A higher fee/cltv may find alternative, indirect routes.
202+ FailureReason .LocalTrampolineFailure (trampolineFeesFailure)
203+ case _ if routeNotFound =>
204+ // If we couldn't find routes, it's likely that the fee/cltv was insufficient.
205+ FailureReason .LocalTrampolineFailure (trampolineFeesFailure)
197206 case _ =>
198- // Otherwise, we try to find a downstream error that we could decrypt.
199- val outgoingNodeFailure = nextPayload match {
200- case nextPayload : IntermediatePayload .NodeRelay .Standard => failures.collectFirst { case RemoteFailure (_, _, e) if e.originNode == nextPayload.outgoingNodeId => e.failureMessage }
201- case nextPayload : IntermediatePayload .NodeRelay .ToNonTrampoline => failures.collectFirst { case RemoteFailure (_, _, e) if e.originNode == nextPayload.outgoingNodeId => e.failureMessage }
207+ nextPayload match {
208+ case _ : IntermediatePayload .NodeRelay .Standard =>
209+ // If we received a failure from the next trampoline node, we won't be able to decrypt it: we should encrypt
210+ // it with our trampoline shared secret and relay it upstream, because only the sender can decrypt it.
211+ failures.collectFirst { case UnreadableRemoteFailure (_, _, packet) => FailureReason .EncryptedDownstreamFailure (packet) }
212+ .getOrElse(FailureReason .LocalTrampolineFailure (TemporaryTrampolineFailure ()))
213+ case nextPayload : IntermediatePayload .NodeRelay .ToNonTrampoline =>
214+ // The recipient doesn't support trampoline: if we received a failure from them, we forward it upstream.
215+ failures.collectFirst { case RemoteFailure (_, _, e) if e.originNode == nextPayload.outgoingNodeId => FailureReason .LocalFailure (e.failureMessage) }
216+ .getOrElse(FailureReason .LocalTrampolineFailure (TemporaryTrampolineFailure ()))
202217 // When using blinded paths, we will never get a failure from the final node (for privacy reasons).
203- case _ : IntermediatePayload .NodeRelay .Blinded => None
204- case _ : IntermediatePayload .NodeRelay .ToBlindedPaths => None
218+ case _ : IntermediatePayload .NodeRelay .Blinded => FailureReason . LocalTrampolineFailure ( TemporaryTrampolineFailure ())
219+ case _ : IntermediatePayload .NodeRelay .ToBlindedPaths => FailureReason . LocalTrampolineFailure ( TemporaryTrampolineFailure ())
205220 }
206- val otherNodeFailure = failures.collectFirst { case RemoteFailure (_, _, e) => e.failureMessage }
207- val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure ()))
208- Some (failure)
209221 }
210222 }
211223
@@ -245,15 +257,17 @@ class NodeRelay private(nodeParams: NodeParams,
245257 case WrappedMultiPartPaymentFailed (MultiPartPaymentFSM .MultiPartPaymentFailed (_, failure, parts)) =>
246258 context.log.warn(" could not complete incoming multi-part payment (parts={} paidAmount={} failure={})" , parts.size, parts.map(_.amount).sum, failure)
247259 Metrics .recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags .RelayType .Trampoline )
248- parts.collect { case p : MultiPartPaymentFSM .HtlcPart => rejectHtlc(p.htlc.id, p.htlc.channelId, p.amount, Some (failure)) }
260+ // Note that we don't treat this as a trampoline failure, which would be encrypted for the payer.
261+ // This is a failure of the previous trampoline node who didn't send a valid MPP payment.
262+ parts.collect { case p : MultiPartPaymentFSM .HtlcPart => rejectHtlc(p.htlc.id, p.htlc.channelId, p.amount, Some (FailureReason .LocalFailure (failure))) }
249263 stopping()
250264 case WrappedMultiPartPaymentSucceeded (MultiPartPaymentFSM .MultiPartPaymentSucceeded (_, parts)) =>
251265 context.log.info(" completed incoming multi-part payment with parts={} paidAmount={}" , parts.size, parts.map(_.amount).sum)
252266 val upstream = Upstream .Hot .Trampoline (htlcs.toList)
253267 validateRelay(nodeParams, upstream, nextPayload) match {
254268 case Some (failure) =>
255269 context.log.warn(s " rejecting trampoline payment reason= $failure" )
256- rejectPayment(upstream, Some (failure))
270+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (failure), nextPayload.isLegacy )
257271 stopping()
258272 case None =>
259273 resolveNextNode(upstream, nextPayload, nextPacket_opt)
@@ -288,7 +302,7 @@ class NodeRelay private(nodeParams: NodeParams,
288302 ensureRecipientReady(upstream, recipient, nextPayload, nextPacket_opt)
289303 case WrappedOutgoingNodeId (None ) =>
290304 context.log.warn(" rejecting trampoline payment to blinded trampoline: cannot identify next node for scid={}" , payloadOut.outgoing)
291- rejectPayment(upstream, Some (UnknownNextPeer ()))
305+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (UnknownNextPeer ()), nextPayload.isLegacy )
292306 stopping()
293307 }
294308 }
@@ -308,7 +322,7 @@ class NodeRelay private(nodeParams: NodeParams,
308322 rejectExtraHtlcPartialFunction orElse {
309323 case WrappedResolvedPaths (resolved) if resolved.isEmpty =>
310324 context.log.warn(" rejecting trampoline payment to blinded paths: no usable blinded path" )
311- rejectPayment(upstream, Some (UnknownNextPeer ()))
325+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (UnknownNextPeer ()), nextPayload.isLegacy )
312326 stopping()
313327 case WrappedResolvedPaths (resolved) =>
314328 // We don't have access to the invoice: we use the only node_id that somewhat makes sense for the recipient.
@@ -344,7 +358,7 @@ class NodeRelay private(nodeParams: NodeParams,
344358 rejectExtraHtlcPartialFunction orElse {
345359 case WrappedPeerReadyResult (_ : PeerReadyNotifier .PeerUnavailable ) =>
346360 context.log.warn(" rejecting payment: failed to wake-up remote peer" )
347- rejectPayment(upstream, Some (UnknownNextPeer ()))
361+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (UnknownNextPeer ()), nextPayload.isLegacy )
348362 stopping()
349363 case WrappedPeerReadyResult (r : PeerReadyNotifier .PeerReady ) =>
350364 relay(upstream, recipient, Some (walletNodeId), Some (r.remoteFeatures), nextPayload, nextPacket_opt)
@@ -420,7 +434,7 @@ class NodeRelay private(nodeParams: NodeParams,
420434 context.log.info(" trampoline payment failed, attempting on-the-fly funding" )
421435 attemptOnTheFlyFunding(upstream, walletNodeId, recipient, nextPayload, failures, startedAt)
422436 case _ =>
423- rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload))
437+ rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload), nextPayload.isLegacy )
424438 recordRelayDuration(startedAt, isSuccess = false )
425439 stopping()
426440 }
@@ -443,7 +457,7 @@ class NodeRelay private(nodeParams: NodeParams,
443457 OutgoingPaymentPacket .buildOutgoingPayment(Origin .Hot (ActorRef .noSender, upstream), paymentHash, dummyRoute, recipient, 1.0 ) match {
444458 case Left (f) =>
445459 context.log.warn(" could not create payment onion for on-the-fly funding: {}" , f.getMessage)
446- rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload))
460+ rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload), nextPayload.isLegacy )
447461 recordRelayDuration(startedAt, isSuccess = false )
448462 stopping()
449463 case Right (nextPacket) =>
@@ -462,7 +476,7 @@ class NodeRelay private(nodeParams: NodeParams,
462476 stopping()
463477 case ProposeOnTheFlyFundingResponse .NotAvailable (reason) =>
464478 context.log.warn(" could not propose on-the-fly funding: {}" , reason)
465- rejectPayment(upstream, Some (UnknownNextPeer ()))
479+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (UnknownNextPeer ()), nextPayload.isLegacy )
466480 recordRelayDuration(startedAt, isSuccess = false )
467481 stopping()
468482 }
@@ -501,15 +515,30 @@ class NodeRelay private(nodeParams: NodeParams,
501515 rejectHtlc(add.id, add.channelId, add.amountMsat)
502516 }
503517
504- private def rejectHtlc (htlcId : Long , channelId : ByteVector32 , amount : MilliSatoshi , failure : Option [FailureMessage ] = None ): Unit = {
505- val failureMessage = failure .getOrElse(IncorrectOrUnknownPaymentDetails (amount, nodeParams.currentBlockHeight))
506- val cmd = CMD_FAIL_HTLC (htlcId, FailureReason . LocalFailure (failureMessage) , commit = true )
518+ private def rejectHtlc (htlcId : Long , channelId : ByteVector32 , amount : MilliSatoshi , failure_opt : Option [FailureReason ] = None ): Unit = {
519+ val failure = failure_opt .getOrElse(FailureReason . LocalFailure ( IncorrectOrUnknownPaymentDetails (amount, nodeParams.currentBlockHeight) ))
520+ val cmd = CMD_FAIL_HTLC (htlcId, failure , commit = true )
507521 PendingCommandsDb .safeSend(register, nodeParams.db.pendingCommands, channelId, cmd)
508522 }
509523
510- private def rejectPayment (upstream : Upstream .Hot .Trampoline , failure : Option [FailureMessage ]): Unit = {
511- Metrics .recordPaymentRelayFailed(failure.map(_.getClass.getSimpleName).getOrElse(" Unknown" ), Tags .RelayType .Trampoline )
512- upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, failure))
524+ private def rejectPayment (upstream : Upstream .Hot .Trampoline , failure : FailureReason , isLegacy : Boolean ): Unit = {
525+ val failure1 = failure match {
526+ case failure : FailureReason .EncryptedDownstreamFailure =>
527+ Metrics .recordPaymentRelayFailed(" Unknown" , Tags .RelayType .Trampoline )
528+ failure
529+ case failure : FailureReason .LocalFailure =>
530+ Metrics .recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags .RelayType .Trampoline )
531+ failure
532+ case failure : FailureReason .LocalTrampolineFailure =>
533+ Metrics .recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags .RelayType .Trampoline )
534+ if (isLegacy) {
535+ // The payer won't be able to decrypt our trampoline failure: we use a legacy failure for backwards-compat.
536+ FailureReason .LocalFailure (LegacyTrampolineFeeInsufficient ())
537+ } else {
538+ failure
539+ }
540+ }
541+ upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, Some (failure1)))
513542 }
514543
515544 private def fulfillPayment (upstream : Upstream .Hot .Trampoline , paymentPreimage : ByteVector32 ): Unit = upstream.received.foreach(r => {
0 commit comments