Skip to content

Commit c9a93eb

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 33406d4 commit c9a93eb

File tree

13 files changed

+587
-154
lines changed

13 files changed

+587
-154
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ data class CannotSignDisconnected (override val channelId: Byte
8787
data class UnexpectedRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "received unexpected RevokeAndAck message")
8888
data class InvalidRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "invalid revocation")
8989
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")
90+
data class CannotDecryptFailure (override val channelId: ByteVector32) : ChannelException(channelId, "cannot decrypt failure packet")
9191
data class PleasePublishYourCommitment (override val channelId: ByteVector32) : ChannelException(channelId, "please publish your local commitment")
9292
data class CommandUnavailableInThisState (override val channelId: ByteVector32, val state: String) : ChannelException(channelId, "cannot execute command in state=$state")
9393
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" }
@@ -82,20 +83,21 @@ data class OutgoingPaymentFailure(val reason: FinalFailure, val failures: List<L
8283
is Either.Right -> when (failure.value) {
8384
is AmountBelowMinimum -> LightningOutgoingPayment.Part.Status.Failed.Failure.PaymentAmountTooSmall
8485
is FeeInsufficient -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
85-
TrampolineExpiryTooSoon -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
86-
TrampolineFeeInsufficient -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
86+
is TrampolineFeeOrExpiryInsufficient -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
8787
is FinalIncorrectCltvExpiry -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientRejectedPayment
8888
is FinalIncorrectHtlcAmount -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientRejectedPayment
8989
is IncorrectOrUnknownPaymentDetails -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientRejectedPayment
9090
PaymentTimeout -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientLiquidityIssue
9191
UnknownNextPeer -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientIsOffline
92+
UnknownNextTrampoline -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientIsOffline
9293
is ExpiryTooSoon -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9394
ExpiryTooFar -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9495
is ChannelDisabled -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9596
is TemporaryChannelFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9697
TemporaryNodeFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9798
PermanentChannelFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
9899
PermanentNodeFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
100+
TemporaryTrampolineFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
99101
is InvalidOnionBlinding -> LightningOutgoingPayment.Part.Status.Failed.Failure.Uninterpretable(failure.value.message)
100102
is InvalidOnionHmac -> LightningOutgoingPayment.Part.Status.Failed.Failure.Uninterpretable(failure.value.message)
101103
is InvalidOnionKey -> LightningOutgoingPayment.Part.Status.Failed.Failure.Uninterpretable(failure.value.message)

0 commit comments

Comments
 (0)