@@ -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 {
@@ -167,31 +170,40 @@ object NodeRelay {
167170 * This helper method translates relaying errors (returned by the downstream nodes) to a BOLT 4 standard error that we
168171 * should return upstream.
169172 */
170- private def translateError (nodeParams : NodeParams , failures : Seq [PaymentFailure ], upstream : Upstream .Hot .Trampoline , nextPayload : IntermediatePayload .NodeRelay ): Option [ FailureMessage ] = {
173+ private def translateError (nodeParams : NodeParams , failures : Seq [PaymentFailure ], upstream : Upstream .Hot .Trampoline , nextPayload : IntermediatePayload .NodeRelay ): FailureReason = {
171174 val amountOut = outgoingAmount(upstream, nextPayload)
172175 val routeNotFound = failures.collectFirst { case f@ LocalFailure (_, _, RouteNotFound ) => f }.nonEmpty
173176 val routingFeeHigh = upstream.amountIn - amountOut >= nodeFee(nodeParams.relayParams.minTrampolineFees, amountOut) * 5
177+ val trampolineFeesFailure = TrampolineFeeOrExpiryInsufficient (nodeParams.relayParams.minTrampolineFees.feeBase * 5 , nodeParams.relayParams.minTrampolineFees.feeProportionalMillionths * 5 , nodeParams.channelConf.expiryDelta * 5 )
178+ // We select the best error we can from our downstream attempts.
174179 failures match {
175- case Nil => None
180+ case Nil => FailureReason . LocalTrampolineFailure ( TemporaryTrampolineFailure ())
176181 case LocalFailure (_, _, BalanceTooLow ) :: Nil if routingFeeHigh =>
177182 // We have direct channels to the target node, but not enough outgoing liquidity to use those channels.
178- // The routing fee proposed by the sender was high enough to find alternative, indirect routes, but didn't yield
179- // any result so we tell them that we don't have enough outgoing liquidity at the moment.
180- Some (TemporaryNodeFailure ())
181- case LocalFailure (_, _, BalanceTooLow ) :: Nil => Some (TrampolineFeeInsufficient ()) // a higher fee/cltv may find alternative, indirect routes
182- case _ if routeNotFound => Some (TrampolineFeeInsufficient ()) // if we couldn't find routes, it's likely that the fee/cltv was insufficient
183+ // The routing fee proposed by the sender was high enough to find alternative, indirect routes, but didn't
184+ // yield any result so we tell them that we don't have enough outgoing liquidity at the moment.
185+ FailureReason .LocalTrampolineFailure (TemporaryTrampolineFailure ())
186+ case LocalFailure (_, _, BalanceTooLow ) :: Nil =>
187+ // A higher fee/cltv may find alternative, indirect routes.
188+ FailureReason .LocalTrampolineFailure (trampolineFeesFailure)
189+ case _ if routeNotFound =>
190+ // If we couldn't find routes, it's likely that the fee/cltv was insufficient.
191+ FailureReason .LocalTrampolineFailure (trampolineFeesFailure)
183192 case _ =>
184- // Otherwise, we try to find a downstream error that we could decrypt.
185- val outgoingNodeFailure = nextPayload match {
186- case nextPayload : IntermediatePayload .NodeRelay .Standard => failures.collectFirst { case RemoteFailure (_, _, e) if e.originNode == nextPayload.outgoingNodeId => e.failureMessage }
187- case nextPayload : IntermediatePayload .NodeRelay .ToNonTrampoline => failures.collectFirst { case RemoteFailure (_, _, e) if e.originNode == nextPayload.outgoingNodeId => e.failureMessage }
193+ nextPayload match {
194+ case _ : IntermediatePayload .NodeRelay .Standard =>
195+ // If we received a failure from the next trampoline node, we won't be able to decrypt it: we should encrypt
196+ // it with our trampoline shared secret and relay it upstream, because only the sender can decrypt it.
197+ failures.collectFirst { case UnreadableRemoteFailure (_, _, packet) => FailureReason .EncryptedDownstreamFailure (packet) }
198+ .getOrElse(FailureReason .LocalTrampolineFailure (TemporaryTrampolineFailure ()))
199+ case nextPayload : IntermediatePayload .NodeRelay .ToNonTrampoline =>
200+ // The recipient doesn't support trampoline: if we received a failure from them, we forward it upstream.
201+ failures.collectFirst { case RemoteFailure (_, _, e) if e.originNode == nextPayload.outgoingNodeId => FailureReason .LocalFailure (e.failureMessage) }
202+ .getOrElse(FailureReason .LocalTrampolineFailure (TemporaryTrampolineFailure ()))
188203 // When using blinded paths, we will never get a failure from the final node (for privacy reasons).
189- case _ : IntermediatePayload .NodeRelay .Blinded => None
190- case _ : IntermediatePayload .NodeRelay .ToBlindedPaths => None
204+ case _ : IntermediatePayload .NodeRelay .Blinded => FailureReason . LocalTrampolineFailure ( TemporaryTrampolineFailure ())
205+ case _ : IntermediatePayload .NodeRelay .ToBlindedPaths => FailureReason . LocalTrampolineFailure ( TemporaryTrampolineFailure ())
191206 }
192- val otherNodeFailure = failures.collectFirst { case RemoteFailure (_, _, e) => e.failureMessage }
193- val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure ()))
194- Some (failure)
195207 }
196208 }
197209
@@ -231,15 +243,17 @@ class NodeRelay private(nodeParams: NodeParams,
231243 case WrappedMultiPartPaymentFailed (MultiPartPaymentFSM .MultiPartPaymentFailed (_, failure, parts)) =>
232244 context.log.warn(" could not complete incoming multi-part payment (parts={} paidAmount={} failure={})" , parts.size, parts.map(_.amount).sum, failure)
233245 Metrics .recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags .RelayType .Trampoline )
234- parts.collect { case p : MultiPartPaymentFSM .HtlcPart => rejectHtlc(p.htlc.id, p.htlc.channelId, p.amount, Some (failure)) }
246+ // Note that we don't treat this as a trampoline failure, which would be encrypted for the payer.
247+ // This is a failure of the previous trampoline node who didn't send a valid MPP payment.
248+ parts.collect { case p : MultiPartPaymentFSM .HtlcPart => rejectHtlc(p.htlc.id, p.htlc.channelId, p.amount, Some (FailureReason .LocalFailure (failure))) }
235249 stopping()
236250 case WrappedMultiPartPaymentSucceeded (MultiPartPaymentFSM .MultiPartPaymentSucceeded (_, parts)) =>
237251 context.log.info(" completed incoming multi-part payment with parts={} paidAmount={}" , parts.size, parts.map(_.amount).sum)
238252 val upstream = Upstream .Hot .Trampoline (htlcs.toList)
239253 validateRelay(nodeParams, upstream, nextPayload) match {
240254 case Some (failure) =>
241255 context.log.warn(s " rejecting trampoline payment reason= $failure" )
242- rejectPayment(upstream, Some (failure))
256+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (failure), nextPayload.isLegacy )
243257 stopping()
244258 case None =>
245259 resolveNextNode(upstream, nextPayload, nextPacket_opt)
@@ -274,7 +288,7 @@ class NodeRelay private(nodeParams: NodeParams,
274288 attemptWakeUpIfRecipientIsWallet(upstream, recipient, nextPayload, nextPacket_opt)
275289 case WrappedOutgoingNodeId (None ) =>
276290 context.log.warn(" rejecting trampoline payment to blinded trampoline: cannot identify next node for scid={}" , payloadOut.outgoing)
277- rejectPayment(upstream, Some (UnknownNextPeer ()))
291+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (UnknownNextPeer ()), nextPayload.isLegacy )
278292 stopping()
279293 }
280294 }
@@ -294,7 +308,7 @@ class NodeRelay private(nodeParams: NodeParams,
294308 rejectExtraHtlcPartialFunction orElse {
295309 case WrappedResolvedPaths (resolved) if resolved.isEmpty =>
296310 context.log.warn(" rejecting trampoline payment to blinded paths: no usable blinded path" )
297- rejectPayment(upstream, Some (UnknownNextPeer ()))
311+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (UnknownNextPeer ()), nextPayload.isLegacy )
298312 stopping()
299313 case WrappedResolvedPaths (resolved) =>
300314 // We don't have access to the invoice: we use the only node_id that somewhat makes sense for the recipient.
@@ -349,7 +363,7 @@ class NodeRelay private(nodeParams: NodeParams,
349363 rejectExtraHtlcPartialFunction orElse {
350364 case WrappedPeerReadyResult (_ : PeerReadyNotifier .PeerUnavailable ) =>
351365 context.log.warn(" rejecting payment: failed to wake-up remote peer" )
352- rejectPayment(upstream, Some (UnknownNextPeer ()))
366+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (UnknownNextPeer ()), nextPayload.isLegacy )
353367 stopping()
354368 case WrappedPeerReadyResult (r : PeerReadyNotifier .PeerReady ) =>
355369 relay(upstream, recipient, Some (walletNodeId), Some (r.remoteFeatures), nextPayload, nextPacket_opt)
@@ -426,7 +440,7 @@ class NodeRelay private(nodeParams: NodeParams,
426440 context.log.info(" trampoline payment failed, attempting on-the-fly funding" )
427441 attemptOnTheFlyFunding(upstream, walletNodeId, recipient, nextPayload, failures, startedAt)
428442 case _ =>
429- rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload))
443+ rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload), nextPayload.isLegacy )
430444 recordRelayDuration(startedAt, isSuccess = false )
431445 stopping()
432446 }
@@ -449,7 +463,7 @@ class NodeRelay private(nodeParams: NodeParams,
449463 OutgoingPaymentPacket .buildOutgoingPayment(Origin .Hot (ActorRef .noSender, upstream), paymentHash, dummyRoute, recipient, 1.0 ) match {
450464 case Left (f) =>
451465 context.log.warn(" could not create payment onion for on-the-fly funding: {}" , f.getMessage)
452- rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload))
466+ rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload), nextPayload.isLegacy )
453467 recordRelayDuration(startedAt, isSuccess = false )
454468 stopping()
455469 case Right (nextPacket) =>
@@ -468,7 +482,7 @@ class NodeRelay private(nodeParams: NodeParams,
468482 stopping()
469483 case ProposeOnTheFlyFundingResponse .NotAvailable (reason) =>
470484 context.log.warn(" could not propose on-the-fly funding: {}" , reason)
471- rejectPayment(upstream, Some (UnknownNextPeer ()))
485+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (UnknownNextPeer ()), nextPayload.isLegacy )
472486 recordRelayDuration(startedAt, isSuccess = false )
473487 stopping()
474488 }
@@ -507,15 +521,30 @@ class NodeRelay private(nodeParams: NodeParams,
507521 rejectHtlc(add.id, add.channelId, add.amountMsat)
508522 }
509523
510- private def rejectHtlc (htlcId : Long , channelId : ByteVector32 , amount : MilliSatoshi , failure : Option [FailureMessage ] = None ): Unit = {
511- val failureMessage = failure .getOrElse(IncorrectOrUnknownPaymentDetails (amount, nodeParams.currentBlockHeight))
512- val cmd = CMD_FAIL_HTLC (htlcId, FailureReason . LocalFailure (failureMessage) , commit = true )
524+ private def rejectHtlc (htlcId : Long , channelId : ByteVector32 , amount : MilliSatoshi , failure_opt : Option [FailureReason ] = None ): Unit = {
525+ val failure = failure_opt .getOrElse(FailureReason . LocalFailure ( IncorrectOrUnknownPaymentDetails (amount, nodeParams.currentBlockHeight) ))
526+ val cmd = CMD_FAIL_HTLC (htlcId, failure , commit = true )
513527 PendingCommandsDb .safeSend(register, nodeParams.db.pendingCommands, channelId, cmd)
514528 }
515529
516- private def rejectPayment (upstream : Upstream .Hot .Trampoline , failure : Option [FailureMessage ]): Unit = {
517- Metrics .recordPaymentRelayFailed(failure.map(_.getClass.getSimpleName).getOrElse(" Unknown" ), Tags .RelayType .Trampoline )
518- upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, failure))
530+ private def rejectPayment (upstream : Upstream .Hot .Trampoline , failure : FailureReason , isLegacy : Boolean ): Unit = {
531+ val failure1 = failure match {
532+ case failure : FailureReason .EncryptedDownstreamFailure =>
533+ Metrics .recordPaymentRelayFailed(" Unknown" , Tags .RelayType .Trampoline )
534+ failure
535+ case failure : FailureReason .LocalFailure =>
536+ Metrics .recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags .RelayType .Trampoline )
537+ failure
538+ case failure : FailureReason .LocalTrampolineFailure =>
539+ Metrics .recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags .RelayType .Trampoline )
540+ if (isLegacy) {
541+ // The payer won't be able to decrypt our trampoline failure: we use a legacy failure for backwards-compat.
542+ FailureReason .LocalFailure (LegacyTrampolineFeeInsufficient ())
543+ } else {
544+ failure
545+ }
546+ }
547+ upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, Some (failure1)))
519548 }
520549
521550 private def fulfillPayment (upstream : Upstream .Hot .Trampoline , paymentPreimage : ByteVector32 ): Unit = upstream.received.foreach(r => {
0 commit comments