@@ -126,12 +126,15 @@ object NodeRelay {
126
126
val amountOut = outgoingAmount(upstream, payloadOut)
127
127
val expiryOut = outgoingExpiry(upstream, payloadOut)
128
128
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 )
129
132
if (upstream.amountIn - amountOut < fee) {
130
- Some (TrampolineFeeInsufficient () )
133
+ Some (failure )
131
134
} else if (upstream.expiryIn - expiryOut < nodeParams.channelConf.expiryDelta) {
132
- Some (TrampolineExpiryTooSoon () )
135
+ Some (failure )
133
136
} else if (expiryOut <= CltvExpiry (nodeParams.currentBlockHeight)) {
134
- Some (TrampolineExpiryTooSoon () )
137
+ Some (failure )
135
138
} else if (amountOut <= MilliSatoshi (0 )) {
136
139
Some (InvalidOnionPayload (UInt64 (2 ), 0 ))
137
140
} else {
@@ -181,31 +184,40 @@ object NodeRelay {
181
184
* This helper method translates relaying errors (returned by the downstream nodes) to a BOLT 4 standard error that we
182
185
* should return upstream.
183
186
*/
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 = {
185
188
val amountOut = outgoingAmount(upstream, nextPayload)
186
189
val routeNotFound = failures.collectFirst { case f@ LocalFailure (_, _, RouteNotFound ) => f }.nonEmpty
187
190
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.
188
193
failures match {
189
- case Nil => None
194
+ case Nil => FailureReason . LocalTrampolineFailure ( TemporaryTrampolineFailure ())
190
195
case LocalFailure (_, _, BalanceTooLow ) :: Nil if routingFeeHigh =>
191
196
// 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)
197
206
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 ()))
202
217
// 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 ())
205
220
}
206
- val otherNodeFailure = failures.collectFirst { case RemoteFailure (_, _, e) => e.failureMessage }
207
- val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure ()))
208
- Some (failure)
209
221
}
210
222
}
211
223
@@ -245,15 +257,17 @@ class NodeRelay private(nodeParams: NodeParams,
245
257
case WrappedMultiPartPaymentFailed (MultiPartPaymentFSM .MultiPartPaymentFailed (_, failure, parts)) =>
246
258
context.log.warn(" could not complete incoming multi-part payment (parts={} paidAmount={} failure={})" , parts.size, parts.map(_.amount).sum, failure)
247
259
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))) }
249
263
stopping()
250
264
case WrappedMultiPartPaymentSucceeded (MultiPartPaymentFSM .MultiPartPaymentSucceeded (_, parts)) =>
251
265
context.log.info(" completed incoming multi-part payment with parts={} paidAmount={}" , parts.size, parts.map(_.amount).sum)
252
266
val upstream = Upstream .Hot .Trampoline (htlcs.toList)
253
267
validateRelay(nodeParams, upstream, nextPayload) match {
254
268
case Some (failure) =>
255
269
context.log.warn(s " rejecting trampoline payment reason= $failure" )
256
- rejectPayment(upstream, Some (failure))
270
+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (failure), nextPayload.isLegacy )
257
271
stopping()
258
272
case None =>
259
273
resolveNextNode(upstream, nextPayload, nextPacket_opt)
@@ -288,7 +302,7 @@ class NodeRelay private(nodeParams: NodeParams,
288
302
ensureRecipientReady(upstream, recipient, nextPayload, nextPacket_opt)
289
303
case WrappedOutgoingNodeId (None ) =>
290
304
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 )
292
306
stopping()
293
307
}
294
308
}
@@ -308,7 +322,7 @@ class NodeRelay private(nodeParams: NodeParams,
308
322
rejectExtraHtlcPartialFunction orElse {
309
323
case WrappedResolvedPaths (resolved) if resolved.isEmpty =>
310
324
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 )
312
326
stopping()
313
327
case WrappedResolvedPaths (resolved) =>
314
328
// 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,
344
358
rejectExtraHtlcPartialFunction orElse {
345
359
case WrappedPeerReadyResult (_ : PeerReadyNotifier .PeerUnavailable ) =>
346
360
context.log.warn(" rejecting payment: failed to wake-up remote peer" )
347
- rejectPayment(upstream, Some (UnknownNextPeer ()))
361
+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (UnknownNextPeer ()), nextPayload.isLegacy )
348
362
stopping()
349
363
case WrappedPeerReadyResult (r : PeerReadyNotifier .PeerReady ) =>
350
364
relay(upstream, recipient, Some (walletNodeId), Some (r.remoteFeatures), nextPayload, nextPacket_opt)
@@ -420,7 +434,7 @@ class NodeRelay private(nodeParams: NodeParams,
420
434
context.log.info(" trampoline payment failed, attempting on-the-fly funding" )
421
435
attemptOnTheFlyFunding(upstream, walletNodeId, recipient, nextPayload, failures, startedAt)
422
436
case _ =>
423
- rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload))
437
+ rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload), nextPayload.isLegacy )
424
438
recordRelayDuration(startedAt, isSuccess = false )
425
439
stopping()
426
440
}
@@ -443,7 +457,7 @@ class NodeRelay private(nodeParams: NodeParams,
443
457
OutgoingPaymentPacket .buildOutgoingPayment(Origin .Hot (ActorRef .noSender, upstream), paymentHash, dummyRoute, recipient, 1.0 ) match {
444
458
case Left (f) =>
445
459
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 )
447
461
recordRelayDuration(startedAt, isSuccess = false )
448
462
stopping()
449
463
case Right (nextPacket) =>
@@ -462,7 +476,7 @@ class NodeRelay private(nodeParams: NodeParams,
462
476
stopping()
463
477
case ProposeOnTheFlyFundingResponse .NotAvailable (reason) =>
464
478
context.log.warn(" could not propose on-the-fly funding: {}" , reason)
465
- rejectPayment(upstream, Some (UnknownNextPeer ()))
479
+ rejectPayment(upstream, FailureReason . LocalTrampolineFailure (UnknownNextPeer ()), nextPayload.isLegacy )
466
480
recordRelayDuration(startedAt, isSuccess = false )
467
481
stopping()
468
482
}
@@ -501,15 +515,30 @@ class NodeRelay private(nodeParams: NodeParams,
501
515
rejectHtlc(add.id, add.channelId, add.amountMsat)
502
516
}
503
517
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 )
507
521
PendingCommandsDb .safeSend(register, nodeParams.db.pendingCommands, channelId, cmd)
508
522
}
509
523
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)))
513
542
}
514
543
515
544
private def fulfillPayment (upstream : Upstream .Hot .Trampoline , paymentPreimage : ByteVector32 ): Unit = upstream.received.foreach(r => {
0 commit comments