Skip to content

Commit 793cec9

Browse files
committed
Update trampoline to match spec proposal
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.
1 parent 1a8b468 commit 793cec9

File tree

20 files changed

+243
-224
lines changed

20 files changed

+243
-224
lines changed

src/commonMain/kotlin/fr/acinq/lightning/Features.kt

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ sealed class Feature {
147147
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Invoice)
148148
}
149149

150+
@Serializable
151+
object TrampolinePayment : Feature() {
152+
override val rfcName get() = "trampoline_routing"
153+
override val mandatory get() = 56
154+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
155+
}
156+
150157
// The following features have not been standardised, hence the high feature bits to avoid conflicts.
151158

152159
/** This feature bit should be activated when a node accepts having their channel reserve set to 0. */
@@ -189,15 +196,6 @@ sealed class Feature {
189196
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
190197
}
191198

192-
// The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted,
193-
// we will introduce a new version of trampoline that will work in parallel to this one, until we can safely deprecate it.
194-
@Serializable
195-
object ExperimentalTrampolinePayment : Feature() {
196-
override val rfcName get() = "trampoline_payment_experimental"
197-
override val mandatory get() = 148
198-
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
199-
}
200-
201199
@Serializable
202200
object ExperimentalSplice : Feature() {
203201
override val rfcName get() = "splice_experimental"
@@ -288,7 +286,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
288286
Feature.Quiescence,
289287
Feature.ChannelType,
290288
Feature.PaymentMetadata,
291-
Feature.ExperimentalTrampolinePayment,
289+
Feature.TrampolinePayment,
292290
Feature.ZeroReserveChannels,
293291
Feature.WakeUpNotificationClient,
294292
Feature.WakeUpNotificationProvider,
@@ -327,7 +325,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
327325
Feature.PaymentSecret to listOf(Feature.VariableLengthOnion),
328326
Feature.BasicMultiPartPayment to listOf(Feature.PaymentSecret),
329327
Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey),
330-
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret),
328+
Feature.TrampolinePayment to listOf(Feature.BasicMultiPartPayment),
331329
Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice),
332330
Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding)
333331
)

src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ data class NodeParams(
197197
Feature.Quiescence to FeatureSupport.Mandatory,
198198
Feature.ChannelType to FeatureSupport.Mandatory,
199199
Feature.PaymentMetadata to FeatureSupport.Optional,
200-
Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional,
200+
Feature.TrampolinePayment to FeatureSupport.Optional,
201201
Feature.ZeroReserveChannels to FeatureSupport.Optional,
202202
Feature.WakeUpNotificationClient to FeatureSupport.Optional,
203203
Feature.ChannelBackupClient to FeatureSupport.Optional,

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,14 @@ class OutgoingPaymentHandler(val nodeParams: NodeParams, val walletParams: Walle
352352
is Bolt11Invoice -> {
353353
val minFinalExpiryDelta = paymentRequest.minFinalExpiryDelta ?: Channel.MIN_CLTV_EXPIRY_DELTA
354354
val finalExpiry = nodeParams.paymentRecipientExpiryParams.computeFinalExpiry(currentBlockHeight, minFinalExpiryDelta)
355-
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(request.amount, finalExpiry, paymentRequest.paymentSecret, paymentRequest.paymentMetadata)
356-
val invoiceFeatures = paymentRequest.features
357-
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (invoiceFeatures.hasFeature(Feature.ExperimentalTrampolinePayment)) {
358-
// We may be paying an older version of lightning-kmp that only supports trampoline packets of size 400.
359-
OutgoingPaymentPacket.buildPacket(request.paymentHash, trampolineRoute, finalPayload, 400)
360-
} else {
361-
OutgoingPaymentPacket.buildTrampolineToNonTrampolinePacket(paymentRequest, trampolineRoute, finalPayload)
355+
val (trampolineAmount, trampolineExpiry, trampolineOnion) = when {
356+
paymentRequest.features.hasFeature(Feature.TrampolinePayment) -> {
357+
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(request.amount, finalExpiry, paymentRequest.paymentSecret, paymentRequest.paymentMetadata)
358+
OutgoingPaymentPacket.buildPacket(request.paymentHash, trampolineRoute, finalPayload, null)
359+
}
360+
else -> {
361+
OutgoingPaymentPacket.buildTrampolineToNonTrampolinePacket(paymentRequest, trampolineRoute.last(), request.amount, finalExpiry)
362+
}
362363
}
363364
return Triple(trampolineAmount, trampolineExpiry, trampolineOnion.packet)
364365
}

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

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -56,32 +56,19 @@ object OutgoingPaymentPacket {
5656

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

8774
/**

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

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ sealed class OnionPaymentPayloadTlv : Tlv {
101101
}
102102
}
103103

104+
/** Id of the next node. */
105+
data class OutgoingNodeId(val nodeId: PublicKey) : OnionPaymentPayloadTlv() {
106+
override val tag: Long get() = OutgoingNodeId.tag
107+
override fun write(out: Output) = LightningCodecs.writeBytes(nodeId.value, out)
108+
109+
companion object : TlvValueReader<OutgoingNodeId> {
110+
const val tag: Long = 14
111+
override fun read(input: Input): OutgoingNodeId = OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33)))
112+
}
113+
}
114+
104115
/**
105116
* When payment metadata is included in a Bolt 9 invoice, we should send it as-is to the recipient.
106117
* This lets recipients generate invoices without having to store anything on their side until the invoice is paid.
@@ -128,6 +139,20 @@ sealed class OnionPaymentPayloadTlv : Tlv {
128139
}
129140
}
130141

142+
/** An encrypted trampoline onion packet. */
143+
data class TrampolineOnion(val packet: OnionRoutingPacket) : OnionPaymentPayloadTlv() {
144+
override val tag: Long get() = TrampolineOnion.tag
145+
override fun write(out: Output) = OnionRoutingPacketSerializer(packet.payload.size()).write(packet, out)
146+
147+
companion object : TlvValueReader<TrampolineOnion> {
148+
const val tag: Long = 20
149+
override fun read(input: Input): TrampolineOnion {
150+
val payloadLength = input.availableBytes - 66 // 1 byte version + 33 bytes public key + 32 bytes HMAC
151+
return TrampolineOnion(OnionRoutingPacketSerializer(payloadLength).read(input))
152+
}
153+
}
154+
}
155+
131156
/**
132157
* Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment
133158
* because the final recipient doesn't support trampoline.
@@ -142,17 +167,6 @@ sealed class OnionPaymentPayloadTlv : Tlv {
142167
}
143168
}
144169

145-
/** Id of the next node. */
146-
data class OutgoingNodeId(val nodeId: PublicKey) : OnionPaymentPayloadTlv() {
147-
override val tag: Long get() = OutgoingNodeId.tag
148-
override fun write(out: Output) = LightningCodecs.writeBytes(nodeId.value, out)
149-
150-
companion object : TlvValueReader<OutgoingNodeId> {
151-
const val tag: Long = 66098
152-
override fun read(input: Input): OutgoingNodeId = OutgoingNodeId(PublicKey(LightningCodecs.bytes(input, 33)))
153-
}
154-
}
155-
156170
/**
157171
* Invoice routing hints. Only included for intermediate trampoline nodes when they should convert to a legacy payment
158172
* because the final recipient doesn't support trampoline.
@@ -194,20 +208,6 @@ sealed class OnionPaymentPayloadTlv : Tlv {
194208
}
195209
}
196210

197-
/** An encrypted trampoline onion packet. */
198-
data class TrampolineOnion(val packet: OnionRoutingPacket) : OnionPaymentPayloadTlv() {
199-
override val tag: Long get() = TrampolineOnion.tag
200-
override fun write(out: Output) = OnionRoutingPacketSerializer(packet.payload.size()).write(packet, out)
201-
202-
companion object : TlvValueReader<TrampolineOnion> {
203-
const val tag: Long = 66100
204-
override fun read(input: Input): TrampolineOnion {
205-
val payloadLength = input.availableBytes - 66 // 1 byte version + 33 bytes public key + 32 bytes HMAC
206-
return TrampolineOnion(OnionRoutingPacketSerializer(payloadLength).read(input))
207-
}
208-
}
209-
}
210-
211211
/** Blinded paths to relay the payment to */
212212
data class OutgoingBlindedPaths(val paths: List<Bolt12Invoice.Companion.PaymentBlindedContactInfo>) : OnionPaymentPayloadTlv() {
213213
override val tag: Long get() = OutgoingBlindedPaths.tag
@@ -252,15 +252,15 @@ object PaymentOnion {
252252
OnionPaymentPayloadTlv.AmountToForward.tag to OnionPaymentPayloadTlv.AmountToForward.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
253253
OnionPaymentPayloadTlv.OutgoingCltv.tag to OnionPaymentPayloadTlv.OutgoingCltv.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
254254
OnionPaymentPayloadTlv.OutgoingChannelId.tag to OnionPaymentPayloadTlv.OutgoingChannelId.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
255+
OnionPaymentPayloadTlv.OutgoingNodeId.tag to OnionPaymentPayloadTlv.OutgoingNodeId.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
255256
OnionPaymentPayloadTlv.PaymentData.tag to OnionPaymentPayloadTlv.PaymentData.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
256257
OnionPaymentPayloadTlv.EncryptedRecipientData.tag to OnionPaymentPayloadTlv.EncryptedRecipientData.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
257258
OnionPaymentPayloadTlv.BlindingPoint.tag to OnionPaymentPayloadTlv.BlindingPoint.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
258259
OnionPaymentPayloadTlv.PaymentMetadata.tag to OnionPaymentPayloadTlv.PaymentMetadata.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
259260
OnionPaymentPayloadTlv.TotalAmount.tag to OnionPaymentPayloadTlv.TotalAmount.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
261+
OnionPaymentPayloadTlv.TrampolineOnion.tag to OnionPaymentPayloadTlv.TrampolineOnion.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
260262
OnionPaymentPayloadTlv.InvoiceFeatures.tag to OnionPaymentPayloadTlv.InvoiceFeatures.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
261-
OnionPaymentPayloadTlv.OutgoingNodeId.tag to OnionPaymentPayloadTlv.OutgoingNodeId.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
262263
OnionPaymentPayloadTlv.InvoiceRoutingInfo.tag to OnionPaymentPayloadTlv.InvoiceRoutingInfo.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
263-
OnionPaymentPayloadTlv.TrampolineOnion.tag to OnionPaymentPayloadTlv.TrampolineOnion.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
264264
OnionPaymentPayloadTlv.OutgoingBlindedPaths.tag to OnionPaymentPayloadTlv.OutgoingBlindedPaths.Companion as TlvValueReader<OnionPaymentPayloadTlv>,
265265
)
266266
)

0 commit comments

Comments
 (0)