Skip to content

Commit

Permalink
Update trampoline to match spec proposal
Browse files Browse the repository at this point in the history
We update the trampoline feature to match the official specification
from lightning/bolts#836.

We remove support for the previous version of trampoline, which means
that when paying nodes that use the experimental version, we will use
the trampoline-to-non-trampoline flow instead. Similarly, when older
nodes pay updated nodes, they won't understand the new trampoline
feature bit and will use the trampoline-to-non-trampoline flow.

We update the trampoline-to-non-trampoline flow to remove the unused
trampoline payload in the onion, which saves some space. Note that we
don't want to officially specify this scenario, as it leaks some data
about the recipient to the trampoline node. We rather wait for nodes
to either support trampoline or blinded paths, which fixes this issue.
  • Loading branch information
t-bast committed Jul 15, 2024
1 parent 1a8b468 commit 793cec9
Show file tree
Hide file tree
Showing 20 changed files with 243 additions and 224 deletions.
20 changes: 9 additions & 11 deletions src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,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)
}

// The following features have not been standardised, hence the high feature bits to avoid conflicts.

/** This feature bit should be activated when a node accepts having their channel reserve set to 0. */
Expand Down Expand Up @@ -189,15 +196,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 ExperimentalSplice : Feature() {
override val rfcName get() = "splice_experimental"
Expand Down Expand Up @@ -288,7 +286,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.Quiescence,
Feature.ChannelType,
Feature.PaymentMetadata,
Feature.ExperimentalTrampolinePayment,
Feature.TrampolinePayment,
Feature.ZeroReserveChannels,
Feature.WakeUpNotificationClient,
Feature.WakeUpNotificationProvider,
Expand Down Expand Up @@ -327,7 +325,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.PaymentSecret to listOf(Feature.VariableLengthOnion),
Feature.BasicMultiPartPayment to listOf(Feature.PaymentSecret),
Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey),
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret),
Feature.TrampolinePayment to listOf(Feature.BasicMultiPartPayment),
Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice),
Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding)
)
Expand Down
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ data class NodeParams(
Feature.Quiescence to FeatureSupport.Mandatory,
Feature.ChannelType to FeatureSupport.Mandatory,
Feature.PaymentMetadata to FeatureSupport.Optional,
Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional,
Feature.TrampolinePayment to FeatureSupport.Optional,
Feature.ZeroReserveChannels to FeatureSupport.Optional,
Feature.WakeUpNotificationClient to FeatureSupport.Optional,
Feature.ChannelBackupClient to FeatureSupport.Optional,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,13 +352,14 @@ class OutgoingPaymentHandler(val nodeParams: NodeParams, val walletParams: Walle
is Bolt11Invoice -> {
val minFinalExpiryDelta = paymentRequest.minFinalExpiryDelta ?: Channel.MIN_CLTV_EXPIRY_DELTA
val finalExpiry = nodeParams.paymentRecipientExpiryParams.computeFinalExpiry(currentBlockHeight, minFinalExpiryDelta)
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(request.amount, finalExpiry, paymentRequest.paymentSecret, paymentRequest.paymentMetadata)
val invoiceFeatures = paymentRequest.features
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (invoiceFeatures.hasFeature(Feature.ExperimentalTrampolinePayment)) {
// We may be paying an older version of lightning-kmp that only supports trampoline packets of size 400.
OutgoingPaymentPacket.buildPacket(request.paymentHash, trampolineRoute, finalPayload, 400)
} else {
OutgoingPaymentPacket.buildTrampolineToNonTrampolinePacket(paymentRequest, trampolineRoute, finalPayload)
val (trampolineAmount, trampolineExpiry, trampolineOnion) = when {
paymentRequest.features.hasFeature(Feature.TrampolinePayment) -> {
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(request.amount, finalExpiry, paymentRequest.paymentSecret, paymentRequest.paymentMetadata)
OutgoingPaymentPacket.buildPacket(request.paymentHash, trampolineRoute, finalPayload, null)
}
else -> {
OutgoingPaymentPacket.buildTrampolineToNonTrampolinePacket(paymentRequest, trampolineRoute.last(), request.amount, finalExpiry)
}
}
return Triple(trampolineAmount, trampolineExpiry, trampolineOnion.packet)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,32 +56,19 @@ object OutgoingPaymentPacket {

/**
* Build an encrypted trampoline onion packet when the final recipient doesn't support trampoline.
* The next-to-last trampoline node payload will contain instructions to convert to a legacy payment.
* The trampoline payload will contain instructions to convert to a legacy payment.
* This reveals to the trampoline node who the recipient is and details from the invoice.
* This must be deprecated once recipients support either trampoline or blinded paths.
*
* @param invoice a Bolt11 invoice (features and routing hints will be provided to the next-to-last node).
* @param hops the trampoline hops (including ourselves in the first hop, and the non-trampoline final recipient in the last hop).
* @param finalPayload payload data for the final node (amount, expiry, etc)
* @return a (firstAmount, firstExpiry, onion) triple where:
* - firstAmount is the amount for the trampoline node in the route
* - firstExpiry is the cltv expiry for the first trampoline node in the route
* - the trampoline onion to include in final payload of a normal onion
* @param invoice a Bolt11 invoice (features and routing hints will be provided to the trampoline node).
* @param hop the trampoline hop from the trampoline node to the recipient.
* @param finalAmount amount that should be received by the final recipient.
* @param finalExpiry cltv expiry that should be received by the final recipient.
*/
fun buildTrampolineToNonTrampolinePacket(invoice: Bolt11Invoice, hops: List<NodeHop>, finalPayload: PaymentOnion.FinalPayload.Standard): Triple<MilliSatoshi, CltvExpiry, PacketAndSecrets> {
// NB: the final payload will never reach the recipient, since the next-to-last trampoline hop will convert that to a legacy payment
// We use the smallest final payload possible, otherwise we may overflow the trampoline onion size.
val dummyFinalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, finalPayload.paymentSecret, null)
val (firstAmount, firstExpiry, payloads) = hops.drop(1).reversed().fold(Triple(finalPayload.amount, finalPayload.expiry, listOf<PaymentOnion.PerHopPayload>(dummyFinalPayload))) { triple, hop ->
val (amount, expiry, payloads) = triple
val payload = when (payloads.size) {
// The next-to-last trampoline hop must include invoice data to indicate the conversion to a legacy payment.
1 -> PaymentOnion.RelayToNonTrampolinePayload.create(finalPayload.amount, finalPayload.totalAmount, finalPayload.expiry, hop.nextNodeId, invoice)
else -> PaymentOnion.NodeRelayPayload.create(amount, expiry, hop.nextNodeId)
}
Triple(amount + hop.fee(amount), expiry + hop.cltvExpiryDelta, listOf(payload) + payloads)
}
val nodes = hops.map { it.nextNodeId }
val onion = buildOnion(nodes, payloads, invoice.paymentHash, payloadLength = null)
return Triple(firstAmount, firstExpiry, onion)
fun buildTrampolineToNonTrampolinePacket(invoice: Bolt11Invoice, hop: NodeHop, finalAmount: MilliSatoshi, finalExpiry: CltvExpiry): Triple<MilliSatoshi, CltvExpiry, PacketAndSecrets> {
val payload = PaymentOnion.RelayToNonTrampolinePayload.create(finalAmount, finalAmount, finalExpiry, hop.nextNodeId, invoice)
val onion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, payloadLength = null)
return Triple(finalAmount + hop.fee(finalAmount), finalExpiry + hop.cltvExpiryDelta, onion)
}

/**
Expand Down
54 changes: 27 additions & 27 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/PaymentOnion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ sealed class OnionPaymentPayloadTlv : Tlv {
}
}

/** Id of the next node. */
data class OutgoingNodeId(val nodeId: PublicKey) : OnionPaymentPayloadTlv() {
override val tag: Long get() = OutgoingNodeId.tag
override fun write(out: Output) = LightningCodecs.writeBytes(nodeId.value, out)

companion object : TlvValueReader<OutgoingNodeId> {
const val tag: Long = 14
override fun read(input: Input): OutgoingNodeId = OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33)))
}
}

/**
* When payment metadata is included in a Bolt 9 invoice, we should send it as-is to the recipient.
* This lets recipients generate invoices without having to store anything on their side until the invoice is paid.
Expand Down Expand Up @@ -128,6 +139,20 @@ sealed class OnionPaymentPayloadTlv : Tlv {
}
}

/** An encrypted trampoline onion packet. */
data class TrampolineOnion(val packet: OnionRoutingPacket) : OnionPaymentPayloadTlv() {
override val tag: Long get() = TrampolineOnion.tag
override fun write(out: Output) = OnionRoutingPacketSerializer(packet.payload.size()).write(packet, out)

companion object : TlvValueReader<TrampolineOnion> {
const val tag: Long = 20
override fun read(input: Input): TrampolineOnion {
val payloadLength = input.availableBytes - 66 // 1 byte version + 33 bytes public key + 32 bytes HMAC
return TrampolineOnion(OnionRoutingPacketSerializer(payloadLength).read(input))
}
}
}

/**
* Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment
* because the final recipient doesn't support trampoline.
Expand All @@ -142,17 +167,6 @@ sealed class OnionPaymentPayloadTlv : Tlv {
}
}

/** Id of the next node. */
data class OutgoingNodeId(val nodeId: PublicKey) : OnionPaymentPayloadTlv() {
override val tag: Long get() = OutgoingNodeId.tag
override fun write(out: Output) = LightningCodecs.writeBytes(nodeId.value, out)

companion object : TlvValueReader<OutgoingNodeId> {
const val tag: Long = 66098
override fun read(input: Input): OutgoingNodeId = OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33)))
}
}

/**
* Invoice routing hints. Only included for intermediate trampoline nodes when they should convert to a legacy payment
* because the final recipient doesn't support trampoline.
Expand Down Expand Up @@ -194,20 +208,6 @@ sealed class OnionPaymentPayloadTlv : Tlv {
}
}

/** An encrypted trampoline onion packet. */
data class TrampolineOnion(val packet: OnionRoutingPacket) : OnionPaymentPayloadTlv() {
override val tag: Long get() = TrampolineOnion.tag
override fun write(out: Output) = OnionRoutingPacketSerializer(packet.payload.size()).write(packet, out)

companion object : TlvValueReader<TrampolineOnion> {
const val tag: Long = 66100
override fun read(input: Input): TrampolineOnion {
val payloadLength = input.availableBytes - 66 // 1 byte version + 33 bytes public key + 32 bytes HMAC
return TrampolineOnion(OnionRoutingPacketSerializer(payloadLength).read(input))
}
}
}

/** Blinded paths to relay the payment to */
data class OutgoingBlindedPaths(val paths: List<Bolt12Invoice.Companion.PaymentBlindedContactInfo>) : OnionPaymentPayloadTlv() {
override val tag: Long get() = OutgoingBlindedPaths.tag
Expand Down Expand Up @@ -252,15 +252,15 @@ object PaymentOnion {
OnionPaymentPayloadTlv.AmountToForward.tag to OnionPaymentPayloadTlv.AmountToForward.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.OutgoingCltv.tag to OnionPaymentPayloadTlv.OutgoingCltv.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.OutgoingChannelId.tag to OnionPaymentPayloadTlv.OutgoingChannelId.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.OutgoingNodeId.tag to OnionPaymentPayloadTlv.OutgoingNodeId.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.PaymentData.tag to OnionPaymentPayloadTlv.PaymentData.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.EncryptedRecipientData.tag to OnionPaymentPayloadTlv.EncryptedRecipientData.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.BlindingPoint.tag to OnionPaymentPayloadTlv.BlindingPoint.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.PaymentMetadata.tag to OnionPaymentPayloadTlv.PaymentMetadata.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.TotalAmount.tag to OnionPaymentPayloadTlv.TotalAmount.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.TrampolineOnion.tag to OnionPaymentPayloadTlv.TrampolineOnion.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.InvoiceFeatures.tag to OnionPaymentPayloadTlv.InvoiceFeatures.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.OutgoingNodeId.tag to OnionPaymentPayloadTlv.OutgoingNodeId.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.InvoiceRoutingInfo.tag to OnionPaymentPayloadTlv.InvoiceRoutingInfo.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.TrampolineOnion.tag to OnionPaymentPayloadTlv.TrampolineOnion.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
OnionPaymentPayloadTlv.OutgoingBlindedPaths.tag to OnionPaymentPayloadTlv.OutgoingBlindedPaths.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
)
)
Expand Down
Loading

0 comments on commit 793cec9

Please sign in to comment.