Skip to content

Commit 2a1e84d

Browse files
committed
Add support for trampoline failures
Add support for the trampoline failure messages added to the BOLTs. We also add supports for encrypting failure e2e using the trampoline shared secrets on top of the outer onion shared secrets. This is a work-in-progress: the basic mechanism works, but it needs some clean-up / refactoring.
1 parent a5e3e16 commit 2a1e84d

File tree

6 files changed

+108
-42
lines changed

6 files changed

+108
-42
lines changed

src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentFailure.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,21 @@ data class OutgoingPaymentFailure(val reason: FinalFailure, val failures: List<L
7878
is Either.Right -> when (failure.value) {
7979
is AmountBelowMinimum -> LightningOutgoingPayment.Part.Status.Failure.PaymentAmountTooSmall
8080
is FeeInsufficient -> LightningOutgoingPayment.Part.Status.Failure.NotEnoughFees
81-
TrampolineExpiryTooSoon -> LightningOutgoingPayment.Part.Status.Failure.NotEnoughFees
82-
TrampolineFeeInsufficient -> LightningOutgoingPayment.Part.Status.Failure.NotEnoughFees
81+
is TrampolineFeeOrExpiryInsufficient -> LightningOutgoingPayment.Part.Status.Failure.NotEnoughFees
8382
is FinalIncorrectCltvExpiry -> LightningOutgoingPayment.Part.Status.Failure.RecipientRejectedPayment
8483
is FinalIncorrectHtlcAmount -> LightningOutgoingPayment.Part.Status.Failure.RecipientRejectedPayment
8584
is IncorrectOrUnknownPaymentDetails -> LightningOutgoingPayment.Part.Status.Failure.RecipientRejectedPayment
8685
PaymentTimeout -> LightningOutgoingPayment.Part.Status.Failure.RecipientLiquidityIssue
8786
UnknownNextPeer -> LightningOutgoingPayment.Part.Status.Failure.RecipientIsOffline
87+
UnknownNextTrampoline -> LightningOutgoingPayment.Part.Status.Failure.RecipientIsOffline
8888
is ExpiryTooSoon -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure
8989
ExpiryTooFar -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure
9090
is ChannelDisabled -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure
9191
is TemporaryChannelFailure -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure
9292
TemporaryNodeFailure -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure
9393
PermanentChannelFailure -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure
9494
PermanentNodeFailure -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure
95+
TemporaryTrampolineFailure -> LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure
9596
is InvalidOnionBlinding -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message)
9697
is InvalidOnionHmac -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message)
9798
is InvalidOnionKey -> LightningOutgoingPayment.Part.Status.Failure.Uninterpretable(failure.value.message)

src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandler.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ import fr.acinq.lightning.logging.mdc
2020
import fr.acinq.lightning.router.NodeHop
2121
import fr.acinq.lightning.utils.UUID
2222
import fr.acinq.lightning.utils.msat
23-
import fr.acinq.lightning.wire.FailureMessage
24-
import fr.acinq.lightning.wire.TrampolineExpiryTooSoon
25-
import fr.acinq.lightning.wire.TrampolineFeeInsufficient
26-
import fr.acinq.lightning.wire.UnknownNextPeer
23+
import fr.acinq.lightning.wire.*
2724

2825
class OutgoingPaymentHandler(val nodeParams: NodeParams, val walletParams: WalletParams, val db: OutgoingPaymentsDb) {
2926

@@ -168,8 +165,9 @@ class OutgoingPaymentHandler(val nodeParams: NodeParams, val walletParams: Walle
168165
val trampolineFees = payment.request.trampolineFeesOverride ?: walletParams.trampolineFees
169166
val finalError = when {
170167
trampolineFees.size <= payment.attemptNumber + 1 -> FinalFailure.RetryExhausted
171-
failure == Either.Right(UnknownNextPeer) -> FinalFailure.RecipientUnreachable
172-
failure != Either.Right(TrampolineExpiryTooSoon) && failure != Either.Right(TrampolineFeeInsufficient) -> FinalFailure.UnknownError // non-retriable error
168+
failure == Either.Right(UnknownNextPeer) || failure == Either.Right(UnknownNextTrampoline) -> FinalFailure.RecipientUnreachable
169+
// TODO: take actual fees returned into account (rework the trampoline fees mechanism).
170+
failure != Either.Right(TemporaryTrampolineFailure) && failure.right !is TrampolineFeeOrExpiryInsufficient -> FinalFailure.UnknownError // non-retriable error
173171
else -> null
174172
}
175173
return if (finalError != null) {

src/commonMain/kotlin/fr/acinq/lightning/payment/OutgoingPaymentPacket.kt

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package fr.acinq.lightning.payment
22

33
import fr.acinq.bitcoin.*
44
import fr.acinq.bitcoin.utils.Either
5+
import fr.acinq.bitcoin.utils.flatMap
56
import fr.acinq.lightning.CltvExpiry
67
import fr.acinq.lightning.Feature
78
import fr.acinq.lightning.Lightning
89
import fr.acinq.lightning.MilliSatoshi
910
import fr.acinq.lightning.channel.ChannelCommand
1011
import fr.acinq.lightning.crypto.sphinx.FailurePacket
1112
import fr.acinq.lightning.crypto.sphinx.PacketAndSecrets
13+
import fr.acinq.lightning.crypto.sphinx.SharedSecrets
1214
import fr.acinq.lightning.crypto.sphinx.Sphinx
1315
import fr.acinq.lightning.router.NodeHop
1416
import fr.acinq.lightning.wire.*
@@ -53,7 +55,9 @@ object OutgoingPaymentPacket {
5355
val trampolinePaymentSecret = Lightning.randomBytes32()
5456
val payload = PaymentOnion.FinalPayload.Standard.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, trampolinePaymentSecret, trampolineOnion.packet)
5557
val paymentOnion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, OnionRoutingPacket.PaymentPacketLength)
56-
return Triple(trampolineAmount, trampolineExpiry, paymentOnion)
58+
// We merge the shared secrets from each onion to allow decrypting failure onions.
59+
val sharedSecrets = SharedSecrets(paymentOnion.sharedSecrets.perHopSecrets + trampolineOnion.sharedSecrets.perHopSecrets)
60+
return Triple(trampolineAmount, trampolineExpiry, paymentOnion.copy(sharedSecrets = sharedSecrets))
5761
}
5862

5963
/**
@@ -162,16 +166,16 @@ object OutgoingPaymentPacket {
162166
}
163167

164168
fun buildHtlcFailure(nodeSecret: PrivateKey, paymentHash: ByteVector32, onion: OnionRoutingPacket, reason: ChannelCommand.Htlc.Settlement.Fail.Reason): Either<FailureMessage, ByteVector> {
165-
// we need to decrypt the payment onion to obtain the shared secret to build the error packet
166-
return when (val result = Sphinx.peel(nodeSecret, paymentHash, onion)) {
167-
is Either.Right -> {
168-
val encryptedReason = when (reason) {
169-
is ChannelCommand.Htlc.Settlement.Fail.Reason.Bytes -> FailurePacket.wrap(reason.bytes.toByteArray(), result.value.sharedSecret)
170-
is ChannelCommand.Htlc.Settlement.Fail.Reason.Failure -> FailurePacket.create(result.value.sharedSecret, reason.message)
171-
}
172-
Either.Right(ByteVector(encryptedReason))
169+
return extractSharedSecrets(nodeSecret, paymentHash, onion).map { sharedSecrets ->
170+
val encryptedReason = when (reason) {
171+
is ChannelCommand.Htlc.Settlement.Fail.Reason.Bytes -> FailurePacket.wrap(reason.bytes.toByteArray(), sharedSecrets.first())
172+
is ChannelCommand.Htlc.Settlement.Fail.Reason.Failure -> FailurePacket.create(sharedSecrets.first(), reason.message)
173+
}
174+
if (sharedSecrets.size == 2) {
175+
ByteVector(FailurePacket.wrap(encryptedReason, sharedSecrets.last()))
176+
} else {
177+
ByteVector(encryptedReason)
173178
}
174-
is Either.Left -> Either.Left(result.value)
175179
}
176180
}
177181

@@ -183,4 +187,15 @@ object OutgoingPaymentPacket {
183187
}
184188
}
185189

190+
private fun extractSharedSecrets(nodeSecret: PrivateKey, paymentHash: ByteVector32, onion: OnionRoutingPacket): Either<FailureMessage, List<ByteVector32>> {
191+
// We decrypt the payment onion to obtain the shared secret.
192+
return Sphinx.peel(nodeSecret, paymentHash, onion).flatMap { outer ->
193+
// If it contains a trampoline onion, we decrypt it as well to obtain the shared secret.
194+
when (val trampolineOnion = PaymentOnion.PerHopPayload.read(outer.payload.toByteArray()).map { it.get<OnionPaymentPayloadTlv.TrampolineOnion>() }.right) {
195+
null -> Either.Right(listOf(outer.sharedSecret))
196+
else -> Sphinx.peel(nodeSecret, paymentHash, trampolineOnion.packet).map { listOf(it.sharedSecret, outer.sharedSecret) }
197+
}
198+
}
199+
}
200+
186201
}

src/commonMain/kotlin/fr/acinq/lightning/wire/FailureMessage.kt

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import fr.acinq.bitcoin.io.ByteArrayInput
55
import fr.acinq.bitcoin.io.ByteArrayOutput
66
import fr.acinq.bitcoin.io.Output
77
import fr.acinq.lightning.CltvExpiry
8+
import fr.acinq.lightning.CltvExpiryDelta
89
import fr.acinq.lightning.MilliSatoshi
910
import fr.acinq.lightning.utils.toByteVector32
1011

@@ -41,10 +42,8 @@ sealed class FailureMessage {
4142
UnknownNextPeer.code -> UnknownNextPeer
4243
AmountBelowMinimum.code -> AmountBelowMinimum(MilliSatoshi(LightningCodecs.u64(stream)), readChannelUpdate(stream))
4344
FeeInsufficient.code -> FeeInsufficient(MilliSatoshi(LightningCodecs.u64(stream)), readChannelUpdate(stream))
44-
TrampolineFeeInsufficient.code -> TrampolineFeeInsufficient
4545
IncorrectCltvExpiry.code -> IncorrectCltvExpiry(CltvExpiry(LightningCodecs.u32(stream).toLong()), readChannelUpdate(stream))
4646
ExpiryTooSoon.code -> ExpiryTooSoon(readChannelUpdate(stream))
47-
TrampolineExpiryTooSoon.code -> TrampolineExpiryTooSoon
4847
IncorrectOrUnknownPaymentDetails.code -> {
4948
val amount = if (stream.availableBytes > 0) MilliSatoshi(LightningCodecs.u64(stream)) else MilliSatoshi(0)
5049
val blockHeight = if (stream.availableBytes > 0) LightningCodecs.u32(stream).toLong() else 0L
@@ -56,6 +55,9 @@ sealed class FailureMessage {
5655
ExpiryTooFar.code -> ExpiryTooFar
5756
InvalidOnionPayload.code -> InvalidOnionPayload(LightningCodecs.bigSize(stream), LightningCodecs.u16(stream))
5857
PaymentTimeout.code -> PaymentTimeout
58+
TemporaryTrampolineFailure.code -> TemporaryTrampolineFailure
59+
TrampolineFeeOrExpiryInsufficient.code -> TrampolineFeeOrExpiryInsufficient(MilliSatoshi(LightningCodecs.u32(stream).toLong()), LightningCodecs.u32(stream), CltvExpiryDelta(LightningCodecs.u16(stream)))
60+
UnknownNextTrampoline.code -> UnknownNextTrampoline
5961
else -> UnknownFailureMessage(code)
6062
}
6163
}
@@ -90,13 +92,11 @@ sealed class FailureMessage {
9092
LightningCodecs.writeU64(input.amount.toLong(), out)
9193
writeChannelUpdate(input.update, out)
9294
}
93-
TrampolineFeeInsufficient -> {}
9495
is IncorrectCltvExpiry -> {
9596
LightningCodecs.writeU32(input.expiry.toLong().toInt(), out)
9697
writeChannelUpdate(input.update, out)
9798
}
9899
is ExpiryTooSoon -> writeChannelUpdate(input.update, out)
99-
TrampolineExpiryTooSoon -> {}
100100
is IncorrectOrUnknownPaymentDetails -> {
101101
LightningCodecs.writeU64(input.amount.toLong(), out)
102102
LightningCodecs.writeU32(input.height.toInt(), out)
@@ -114,6 +114,13 @@ sealed class FailureMessage {
114114
LightningCodecs.writeU16(input.offset, out)
115115
}
116116
PaymentTimeout -> {}
117+
TemporaryTrampolineFailure -> {}
118+
is TrampolineFeeOrExpiryInsufficient -> {
119+
LightningCodecs.writeU32(input.feeBase.toLong().toInt(), out)
120+
LightningCodecs.writeU32(input.feeProportionalMillionths, out)
121+
LightningCodecs.writeU16(input.expiryDelta.toInt(), out)
122+
}
123+
UnknownNextTrampoline -> {}
117124
is UnknownFailureMessage -> {}
118125
}
119126
}
@@ -195,10 +202,6 @@ data class FeeInsufficient(val amount: MilliSatoshi, override val update: Channe
195202
override val message get() = "payment fee was below the minimum required by the channel"
196203
companion object { const val code = UPDATE or 12 }
197204
}
198-
object TrampolineFeeInsufficient : FailureMessage(), Node {
199-
override val code get() = NODE or 51
200-
override val message get() = "payment fee was below the minimum required by the trampoline node"
201-
}
202205
data class IncorrectCltvExpiry(val expiry: CltvExpiry, override val update: ChannelUpdate) : FailureMessage(), Update {
203206
override val code get() = IncorrectCltvExpiry.code
204207
override val message get() = "payment expiry doesn't match the value in the onion"
@@ -209,10 +212,6 @@ data class ExpiryTooSoon(override val update: ChannelUpdate) : FailureMessage(),
209212
override val message get() = "payment expiry is too close to the current block height for safe handling by the relaying node"
210213
companion object { const val code = UPDATE or 14 }
211214
}
212-
object TrampolineExpiryTooSoon : FailureMessage(), Node {
213-
override val code get() = NODE or 52
214-
override val message get() = "payment expiry is too close to the current block height for safe handling by the relaying node"
215-
}
216215
data class IncorrectOrUnknownPaymentDetails(val amount: MilliSatoshi, val height: Long) : FailureMessage(), Perm {
217216
override val code get() = IncorrectOrUnknownPaymentDetails.code
218217
override val message get() = "incorrect payment details or unknown payment hash"
@@ -246,6 +245,19 @@ data object PaymentTimeout : FailureMessage() {
246245
override val code get() = 23
247246
override val message get() = "the complete payment amount was not received within a reasonable time"
248247
}
248+
data object TemporaryTrampolineFailure : FailureMessage(), Node {
249+
override val code get() = NODE or 25
250+
override val message get() = "the trampoline node was unable to relay the payment because of downstream temporary failures"
251+
}
252+
data class TrampolineFeeOrExpiryInsufficient(val feeBase: MilliSatoshi, val feeProportionalMillionths: Int, val expiryDelta: CltvExpiryDelta) : FailureMessage(), Node {
253+
override val code get() = TrampolineFeeOrExpiryInsufficient.code
254+
override val message get() = "trampoline fees or expiry are insufficient to relay the payment"
255+
companion object { const val code = NODE or 26 }
256+
}
257+
data object UnknownNextTrampoline : FailureMessage(), Perm {
258+
override val code get() = PERM or 27
259+
override val message get() = "the trampoline node was unable to find the next trampoline node"
260+
}
249261
/**
250262
* We allow remote nodes to send us unknown failure codes (e.g. deprecated failure codes).
251263
* By reading the PERM and NODE bits of the failure code we can still extract useful information for payment retry even

0 commit comments

Comments
 (0)