Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Invoice)
}

@Serializable
object TrampolinePayment : Feature() {
override val rfcName get() = "trampoline_routing"
override val mandatory get() = 56
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice, FeatureScope.Bolt12)
}

@Serializable
object SimpleClose : Feature() {
override val rfcName get() = "option_simple_close"
Expand Down Expand Up @@ -194,15 +201,6 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

// The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted,
// we will introduce a new version of trampoline that will work in parallel to this one, until we can safely deprecate it.
@Serializable
object ExperimentalTrampolinePayment : Feature() {
override val rfcName get() = "trampoline_payment_experimental"
override val mandatory get() = 148
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
}

@Serializable
object OnTheFlyFunding : Feature() {
override val rfcName get() = "on_the_fly_funding"
Expand Down Expand Up @@ -294,9 +292,9 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.ProvideStorage,
Feature.ChannelType,
Feature.PaymentMetadata,
Feature.TrampolinePayment,
Feature.SimpleClose,
Feature.Splicing,
Feature.ExperimentalTrampolinePayment,
Feature.ZeroReserveChannels,
Feature.WakeUpNotificationClient,
Feature.WakeUpNotificationProvider,
Expand Down Expand Up @@ -334,7 +332,6 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.BasicMultiPartPayment to listOf(Feature.PaymentSecret),
Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey),
Feature.SimpleClose to listOf(Feature.ShutdownAnySegwit),
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret),
Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,9 @@ data class NodeParams(
Feature.Quiescence to FeatureSupport.Mandatory,
Feature.ChannelType to FeatureSupport.Mandatory,
Feature.PaymentMetadata to FeatureSupport.Optional,
Feature.TrampolinePayment to FeatureSupport.Optional,
Feature.SimpleClose to FeatureSupport.Mandatory,
Feature.Splicing to FeatureSupport.Mandatory,
Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional,
Feature.ZeroReserveChannels to FeatureSupport.Optional,
Feature.WakeUpNotificationClient to FeatureSupport.Optional,
Feature.OnTheFlyFunding to FeatureSupport.Optional,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ data class CannotSignDisconnected (override val channelId: Byte
data class UnexpectedRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "received unexpected RevokeAndAck message")
data class InvalidRevocation (override val channelId: ByteVector32) : ChannelException(channelId, "invalid revocation")
data class InvalidFailureCode (override val channelId: ByteVector32) : ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
data class CannotDecryptFailure (override val channelId: ByteVector32, val details: String) : ChannelException(channelId, "cannot decrypt failure message: $details")
data class CannotDecryptFailure (override val channelId: ByteVector32) : ChannelException(channelId, "cannot decrypt failure packet")
data class PleasePublishYourCommitment (override val channelId: ByteVector32) : ChannelException(channelId, "please publish your local commitment")
data class CommandUnavailableInThisState (override val channelId: ByteVector32, val state: String) : ChannelException(channelId, "cannot execute command in state=$state")
data class ForbiddenDuringSplice (override val channelId: ByteVector32, val command: String?) : ChannelException(channelId, "cannot process $command while splicing")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,35 @@ import fr.acinq.bitcoin.utils.Either
import fr.acinq.bitcoin.utils.Try
import fr.acinq.bitcoin.utils.runTrying
import fr.acinq.lightning.crypto.ChaCha20
import fr.acinq.lightning.utils.*
import fr.acinq.lightning.utils.toByteVector
import fr.acinq.lightning.utils.toByteVector32
import fr.acinq.lightning.utils.xor
import fr.acinq.lightning.wire.*
import fr.acinq.secp256k1.Hex

/**
* Decrypting an onion packet yields a payload for the current node and the encrypted packet for the next node.
*
* @param payload decrypted payload for this node.
* @param nextPacket packet for the next node.
* @param sharedSecret shared secret for the sending node, which we will need to return failure messages.
*/
data class DecryptedPacket(val payload: ByteVector, val nextPacket: OnionRoutingPacket, val sharedSecret: ByteVector32) {
val isLastPacket: Boolean = nextPacket.hmac == ByteVector32.Zeroes
}

data class SharedSecrets(val perHopSecrets: List<Pair<ByteVector32, PublicKey>>)

data class PacketAndSecrets(val packet: OnionRoutingPacket, val sharedSecrets: SharedSecrets)

/**
* see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md
*/
object Sphinx {
// We use HMAC-SHA256 which returns 32-bytes message authentication codes.
const val MacLength = 32

/**
* Decrypting an onion packet yields a payload for the current node and the encrypted packet for the next node.
*
* @param payload decrypted payload for this node.
* @param nextPacket packet for the next node.
* @param sharedSecret shared secret for the sending node, which we will need to return failure messages.
*/
data class DecryptedPacket(val payload: ByteVector, val nextPacket: OnionRoutingPacket, val sharedSecret: ByteVector32) {
val isLastPacket: Boolean = nextPacket.hmac == ByteVector32.Zeroes
}

/** Shared secret used to encrypt the payload for a given node. */
data class SharedSecret(val secret: ByteVector32, val remoteNodeId: PublicKey)

data class PacketAndSecrets(val packet: OnionRoutingPacket, val sharedSecrets: List<SharedSecret>)

/** Secp256k1's base point. */
private val CurveG = PublicKey(ByteVector("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"))

Expand Down Expand Up @@ -263,18 +266,10 @@ object Sphinx {
}

val packet = loop(payloads.dropLast(1), ephemeralPublicKeys.dropLast(1), sharedsecrets.dropLast(1), lastPacket)
return PacketAndSecrets(packet, SharedSecrets(sharedsecrets.zip(publicKeys)))
return PacketAndSecrets(packet, sharedsecrets.zip(publicKeys).map { SharedSecret(it.first, it.second) })
}
}

/**
* A properly decrypted failure from a node in the route.
*
* @param originNode public key of the node that generated the failure.
* @param failureMessage friendly failure message.
*/
data class DecryptedFailurePacket(val originNode: PublicKey, val failureMessage: FailureMessage)

/**
* An onion-encrypted failure packet from an intermediate node:
* +----------------+----------------------------------+-----------------+----------------------+-----+
Expand All @@ -286,6 +281,21 @@ object FailurePacket {

private const val RecommendedPayloadLength = 256

/**
* A properly decrypted failure from a node in the route.
*
* @param originNode public key of the node that generated the failure.
* @param failureMessage friendly failure message.
*/
data class DecryptedPacket(val originNode: PublicKey, val failureMessage: FailureMessage)

/**
* The downstream failure could not be decrypted.
*
* @param unwrapped encrypted failure packet after unwrapping using our shared secrets.
*/
data class CannotDecryptPacket(val unwrapped: ByteArray)

fun encode(failure: FailureMessage, macKey: ByteVector32, payloadLength: Int = RecommendedPayloadLength): ByteArray {
val out = ByteArrayOutput()
val failureMessageBin = FailureMessage.encode(failure)
Expand Down Expand Up @@ -343,27 +353,21 @@ object FailurePacket {
* it was sent by the corresponding node.
* Note that malicious nodes in the route may have altered the packet, triggering a decryption failure.
*
* @param packet failure packet.
* @param packet failure packet.
* @param sharedSecrets nodes shared secrets.
* @return Success(secret, failure message) if the origin of the packet could be identified and the packet
* decrypted, Failure otherwise.
* @return the decrypted failure message and the failing node if the packet can be decrypted.
*/
fun decrypt(packet: ByteArray, sharedSecrets: SharedSecrets): Try<DecryptedFailurePacket> {
fun loop(packet: ByteArray, secrets: List<Pair<ByteVector32, PublicKey>>): Try<DecryptedFailurePacket> {
return if (secrets.isEmpty()) {
val ex = IllegalArgumentException("couldn't parse error packet=$packet with sharedSecrets=$secrets")
Try.Failure(ex)
} else {
val (secret, pubkey) = secrets.first()
val packet1 = wrap(packet, secret)
val um = Sphinx.generateKey("um", secret)
when (val error = decode(packet1, um)) {
is Try.Failure -> loop(packet1, secrets.tail())
is Try.Success -> Try.Success(DecryptedFailurePacket(pubkey, error.result))
}
tailrec fun decrypt(packet: ByteArray, sharedSecrets: List<Sphinx.SharedSecret>): Either<CannotDecryptPacket, DecryptedPacket> {
return if (sharedSecrets.isEmpty()) {
Either.Left(CannotDecryptPacket(packet))
} else {
val ss = sharedSecrets.first()
val packet1 = wrap(packet, ss.secret)
val um = Sphinx.generateKey("um", ss.secret)
when (val error = decode(packet1, um)) {
is Try.Failure -> decrypt(packet1, sharedSecrets.tail())
is Try.Success -> Either.Right(DecryptedPacket(ss.remoteNodeId, error.result))
}
}

return loop(packet, sharedSecrets.perHopSecrets)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,11 @@ object IncomingPaymentPacket {
when (val trampolineOnion = outer.records.get<OnionPaymentPayloadTlv.TrampolineOnion>()) {
null -> validate(htlcAmount, htlcExpiry, outer)
else -> {
when (val inner = decryptOnion(paymentHash, trampolineOnion.packet, privateKey, null)) {
when (val inner = decryptOnion(paymentHash, trampolineOnion.packet, privateKey, outer.records.get<OnionPaymentPayloadTlv.PathKey>()?.publicKey)) {
is Either.Left -> Either.Left(inner.value)
is Either.Right -> when (val innerPayload = inner.value) {
is PaymentOnion.FinalPayload.Standard -> validate(htlcAmount, htlcExpiry, outer, innerPayload)
// Blinded trampoline paths are not supported.
is PaymentOnion.FinalPayload.Blinded -> Either.Left(InvalidOnionPayload(0, 0))
is PaymentOnion.FinalPayload.Blinded -> validate(htlcAmount, htlcExpiry, trampolineOnion.packet, innerPayload)
}
}
}
Expand All @@ -72,7 +71,6 @@ object IncomingPaymentPacket {
when (val encryptedRecipientData = tlvs.get<OnionPaymentPayloadTlv.EncryptedRecipientData>()?.data) {
null -> when {
pathKey != null -> Either.Left(InvalidOnionBlinding(hash(packet)))
tlvs.get<OnionPaymentPayloadTlv.PathKey>() != null -> Either.Left(InvalidOnionBlinding(hash(packet)))
else -> PaymentOnion.FinalPayload.Standard.read(decrypted.payload)
}
else -> when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ sealed class FinalFailure {
data object NoAvailableChannels : FinalFailure() { override fun toString(): String = "payment could not be sent through existing channels, check individual failures for more details" }
data object InsufficientBalance : FinalFailure() { override fun toString(): String = "not enough funds in wallet to afford payment" }
data object RecipientUnreachable : FinalFailure() { override fun toString(): String = "the recipient was offline or did not have enough liquidity to receive the payment" }
data object RecipientRejectedPayment : FinalFailure() { override fun toString(): String = "the recipient rejected the payment" }
data object RetryExhausted: FinalFailure() { override fun toString(): String = "payment attempts exhausted without success" }
data object WalletRestarted: FinalFailure() { override fun toString(): String = "wallet restarted while a payment was ongoing" }
data object UnknownError : FinalFailure() { override fun toString(): String = "an unknown error occurred" }
Expand Down Expand Up @@ -82,20 +83,21 @@ data class OutgoingPaymentFailure(val reason: FinalFailure, val failures: List<L
is Either.Right -> when (failure.value) {
is AmountBelowMinimum -> LightningOutgoingPayment.Part.Status.Failed.Failure.PaymentAmountTooSmall
is FeeInsufficient -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
TrampolineExpiryTooSoon -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
TrampolineFeeInsufficient -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
is TrampolineFeeOrExpiryInsufficient -> LightningOutgoingPayment.Part.Status.Failed.Failure.NotEnoughFees
is FinalIncorrectCltvExpiry -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientRejectedPayment
is FinalIncorrectHtlcAmount -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientRejectedPayment
is IncorrectOrUnknownPaymentDetails -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientRejectedPayment
PaymentTimeout -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientLiquidityIssue
UnknownNextPeer -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientIsOffline
UnknownNextTrampoline -> LightningOutgoingPayment.Part.Status.Failed.Failure.RecipientIsOffline
is ExpiryTooSoon -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
ExpiryTooFar -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
is ChannelDisabled -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
is TemporaryChannelFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
TemporaryNodeFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
PermanentChannelFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
PermanentNodeFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
TemporaryTrampolineFailure -> LightningOutgoingPayment.Part.Status.Failed.Failure.TemporaryRemoteFailure
is InvalidOnionBlinding -> LightningOutgoingPayment.Part.Status.Failed.Failure.Uninterpretable(failure.value.message)
is InvalidOnionHmac -> LightningOutgoingPayment.Part.Status.Failed.Failure.Uninterpretable(failure.value.message)
is InvalidOnionKey -> LightningOutgoingPayment.Part.Status.Failed.Failure.Uninterpretable(failure.value.message)
Expand Down
Loading
Loading