Skip to content

Commit 02d0cfb

Browse files
committed
Add support for trampoline failures
Add support for the trampoline failure messages added to the BOLTs. We also add support for encrypting failures e2e using the trampoline shared secrets on top of the outer onion shared secrets.
1 parent 9f6fb36 commit 02d0cfb

File tree

14 files changed

+499
-156
lines changed

14 files changed

+499
-156
lines changed

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package fr.acinq.lightning.channel
22

3-
import fr.acinq.bitcoin.BlockHash
4-
import fr.acinq.bitcoin.ByteVector32
5-
import fr.acinq.bitcoin.Satoshi
6-
import fr.acinq.bitcoin.TxId
3+
import fr.acinq.bitcoin.*
74
import fr.acinq.lightning.CltvExpiry
85
import fr.acinq.lightning.CltvExpiryDelta
96
import fr.acinq.lightning.MilliSatoshi
@@ -87,7 +84,7 @@ data class CannotSignDisconnected (override val channelId: Byte
8784
data class UnexpectedRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "received unexpected RevokeAndAck message")
8885
data class InvalidRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "invalid revocation")
8986
data class InvalidFailureCode (override val channelId: ByteVector32) : ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
90-
data class CannotDecryptFailure (override val channelId: ByteVector32, val details: String) : ChannelException(channelId, "cannot decrypt failure message: $details")
87+
data class CannotDecryptFailure (override val channelId: ByteVector32) : ChannelException(channelId, "cannot decrypt failure packet")
9188
data class PleasePublishYourCommitment (override val channelId: ByteVector32) : ChannelException(channelId, "please publish your local commitment")
9289
data class CommandUnavailableInThisState (override val channelId: ByteVector32, val state: String) : ChannelException(channelId, "cannot execute command in state=$state")
9390
data class ForbiddenDuringSplice (override val channelId: ByteVector32, val command: String?) : ChannelException(channelId, "cannot process $command while splicing")

modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/sphinx/Sphinx.kt

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,35 @@ import fr.acinq.bitcoin.utils.Either
1010
import fr.acinq.bitcoin.utils.Try
1111
import fr.acinq.bitcoin.utils.runTrying
1212
import fr.acinq.lightning.crypto.ChaCha20
13-
import fr.acinq.lightning.utils.*
13+
import fr.acinq.lightning.utils.toByteVector
14+
import fr.acinq.lightning.utils.toByteVector32
15+
import fr.acinq.lightning.utils.xor
1416
import fr.acinq.lightning.wire.*
1517
import fr.acinq.secp256k1.Hex
1618

17-
/**
18-
* Decrypting an onion packet yields a payload for the current node and the encrypted packet for the next node.
19-
*
20-
* @param payload decrypted payload for this node.
21-
* @param nextPacket packet for the next node.
22-
* @param sharedSecret shared secret for the sending node, which we will need to return failure messages.
23-
*/
24-
data class DecryptedPacket(val payload: ByteVector, val nextPacket: OnionRoutingPacket, val sharedSecret: ByteVector32) {
25-
val isLastPacket: Boolean = nextPacket.hmac == ByteVector32.Zeroes
26-
}
27-
28-
data class SharedSecrets(val perHopSecrets: List<Pair<ByteVector32, PublicKey>>)
29-
30-
data class PacketAndSecrets(val packet: OnionRoutingPacket, val sharedSecrets: SharedSecrets)
31-
3219
/**
3320
* see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md
3421
*/
3522
object Sphinx {
3623
// We use HMAC-SHA256 which returns 32-bytes message authentication codes.
3724
const val MacLength = 32
3825

26+
/**
27+
* Decrypting an onion packet yields a payload for the current node and the encrypted packet for the next node.
28+
*
29+
* @param payload decrypted payload for this node.
30+
* @param nextPacket packet for the next node.
31+
* @param sharedSecret shared secret for the sending node, which we will need to return failure messages.
32+
*/
33+
data class DecryptedPacket(val payload: ByteVector, val nextPacket: OnionRoutingPacket, val sharedSecret: ByteVector32) {
34+
val isLastPacket: Boolean = nextPacket.hmac == ByteVector32.Zeroes
35+
}
36+
37+
/** Shared secret used to encrypt the payload for a given node. */
38+
data class SharedSecret(val secret: ByteVector32, val remoteNodeId: PublicKey)
39+
40+
data class PacketAndSecrets(val packet: OnionRoutingPacket, val sharedSecrets: List<SharedSecret>)
41+
3942
/** Secp256k1's base point. */
4043
private val CurveG = PublicKey(ByteVector("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"))
4144

@@ -263,18 +266,10 @@ object Sphinx {
263266
}
264267

265268
val packet = loop(payloads.dropLast(1), ephemeralPublicKeys.dropLast(1), sharedsecrets.dropLast(1), lastPacket)
266-
return PacketAndSecrets(packet, SharedSecrets(sharedsecrets.zip(publicKeys)))
269+
return PacketAndSecrets(packet, sharedsecrets.zip(publicKeys).map { SharedSecret(it.first, it.second) })
267270
}
268271
}
269272

270-
/**
271-
* A properly decrypted failure from a node in the route.
272-
*
273-
* @param originNode public key of the node that generated the failure.
274-
* @param failureMessage friendly failure message.
275-
*/
276-
data class DecryptedFailurePacket(val originNode: PublicKey, val failureMessage: FailureMessage)
277-
278273
/**
279274
* An onion-encrypted failure packet from an intermediate node:
280275
* +----------------+----------------------------------+-----------------+----------------------+-----+
@@ -286,6 +281,21 @@ object FailurePacket {
286281

287282
private const val RecommendedPayloadLength = 256
288283

284+
/**
285+
* A properly decrypted failure from a node in the route.
286+
*
287+
* @param originNode public key of the node that generated the failure.
288+
* @param failureMessage friendly failure message.
289+
*/
290+
data class DecryptedPacket(val originNode: PublicKey, val failureMessage: FailureMessage)
291+
292+
/**
293+
* The downstream failure could not be decrypted.
294+
*
295+
* @param unwrapped encrypted failure packet after unwrapping using our shared secrets.
296+
*/
297+
data class CannotDecryptPacket(val unwrapped: ByteArray)
298+
289299
fun encode(failure: FailureMessage, macKey: ByteVector32, payloadLength: Int = RecommendedPayloadLength): ByteArray {
290300
val out = ByteArrayOutput()
291301
val failureMessageBin = FailureMessage.encode(failure)
@@ -343,27 +353,21 @@ object FailurePacket {
343353
* it was sent by the corresponding node.
344354
* Note that malicious nodes in the route may have altered the packet, triggering a decryption failure.
345355
*
346-
* @param packet failure packet.
356+
* @param packet failure packet.
347357
* @param sharedSecrets nodes shared secrets.
348-
* @return Success(secret, failure message) if the origin of the packet could be identified and the packet
349-
* decrypted, Failure otherwise.
358+
* @return the decrypted failure message and the failing node if the packet can be decrypted.
350359
*/
351-
fun decrypt(packet: ByteArray, sharedSecrets: SharedSecrets): Try<DecryptedFailurePacket> {
352-
fun loop(packet: ByteArray, secrets: List<Pair<ByteVector32, PublicKey>>): Try<DecryptedFailurePacket> {
353-
return if (secrets.isEmpty()) {
354-
val ex = IllegalArgumentException("couldn't parse error packet=$packet with sharedSecrets=$secrets")
355-
Try.Failure(ex)
356-
} else {
357-
val (secret, pubkey) = secrets.first()
358-
val packet1 = wrap(packet, secret)
359-
val um = Sphinx.generateKey("um", secret)
360-
when (val error = decode(packet1, um)) {
361-
is Try.Failure -> loop(packet1, secrets.tail())
362-
is Try.Success -> Try.Success(DecryptedFailurePacket(pubkey, error.result))
363-
}
360+
tailrec fun decrypt(packet: ByteArray, sharedSecrets: List<Sphinx.SharedSecret>): Either<CannotDecryptPacket, DecryptedPacket> {
361+
return if (sharedSecrets.isEmpty()) {
362+
Either.Left(CannotDecryptPacket(packet))
363+
} else {
364+
val ss = sharedSecrets.first()
365+
val packet1 = wrap(packet, ss.secret)
366+
val um = Sphinx.generateKey("um", ss.secret)
367+
when (val error = decode(packet1, um)) {
368+
is Try.Failure -> decrypt(packet1, sharedSecrets.tail())
369+
is Try.Success -> Either.Right(DecryptedPacket(ss.remoteNodeId, error.result))
364370
}
365371
}
366-
367-
return loop(packet, sharedSecrets.perHopSecrets)
368372
}
369373
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ sealed class FinalFailure {
2929
data object NoAvailableChannels : FinalFailure() { override fun toString(): String = "payment could not be sent through existing channels, check individual failures for more details" }
3030
data object InsufficientBalance : FinalFailure() { override fun toString(): String = "not enough funds in wallet to afford payment" }
3131
data object RecipientUnreachable : FinalFailure() { override fun toString(): String = "the recipient was offline or did not have enough liquidity to receive the payment" }
32+
data object RecipientRejectedPayment : FinalFailure() { override fun toString(): String = "the recipient rejected the payment" }
3233
data object RetryExhausted: FinalFailure() { override fun toString(): String = "payment attempts exhausted without success" }
3334
data object WalletRestarted: FinalFailure() { override fun toString(): String = "wallet restarted while a payment was ongoing" }
3435
data object UnknownError : FinalFailure() { override fun toString(): String = "an unknown error occurred" }
@@ -81,20 +82,21 @@ data class OutgoingPaymentFailure(val reason: FinalFailure, val failures: List<L
8182
is Either.Right -> when (failure.value) {
8283
is AmountBelowMinimum -> LightningOutgoingPayment.Part.Status.Failed.Failure.PaymentAmountTooSmall
8384
is FeeInsufficient -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
84-
TrampolineExpiryTooSoon -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
85-
TrampolineFeeInsufficient -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
85+
is TrampolineFeeOrExpiryInsufficient -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
8686
is FinalIncorrectCltvExpiry -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientRejectedPayment
8787
is FinalIncorrectHtlcAmount -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientRejectedPayment
8888
is IncorrectOrUnknownPaymentDetails -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientRejectedPayment
8989
PaymentTimeout -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientLiquidityIssue
9090
UnknownNextPeer -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientIsOffline
91+
UnknownNextTrampoline -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientIsOffline
9192
is ExpiryTooSoon -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9293
ExpiryTooFar -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9394
is ChannelDisabled -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9495
is TemporaryChannelFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9596
TemporaryNodeFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9697
PermanentChannelFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9798
PermanentNodeFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
99+
TemporaryTrampolineFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
98100
is InvalidOnionBlinding -> LightningOutgoingPayment.Part.Status.Failed.Failure.Uninterpretable(failure.value.message)
99101
is InvalidOnionHmac -> LightningOutgoingPayment.Part.Status.Failed.Failure.Uninterpretable(failure.value.message)
100102
is InvalidOnionKey -> LightningOutgoingPayment.Part.Status.Failed.Failure.Uninterpretable(failure.value.message)

0 commit comments

Comments
 (0)